From 30a44455e85301e1cdd87837a0c311c6d30cdf0b Mon Sep 17 00:00:00 2001 From: cghislai Date: Sun, 8 Jun 2025 04:18:09 +0200 Subject: [PATCH] WIP --- .../prompts-to-test-spec/.env.example | 13 +- .../prompts-to-test-spec/src/config.ts | 1 + .../src/services/gemini-project-processor.ts | 32 ---- .../src/services/gemini-service.ts | 159 +++++------------- .../src/services/pull-request-service.ts | 12 +- 5 files changed, 58 insertions(+), 159 deletions(-) diff --git a/src/functions/prompts-to-test-spec/.env.example b/src/functions/prompts-to-test-spec/.env.example index 9aaf65c..7f1c9a8 100644 --- a/src/functions/prompts-to-test-spec/.env.example +++ b/src/functions/prompts-to-test-spec/.env.example @@ -20,7 +20,18 @@ GITEA_PASSWORD=your_gitea_password_here # Google Cloud configuration GOOGLE_CLOUD_PROJECT_ID=your_gcp_project_id_here GOOGLE_CLOUD_LOCATION=us-central1 -GEMINI_MODEL=gemini-1.5-pro +GEMINI_MODEL=gemini-2.0-flash-lite-001 + +# Google Cloud Authentication +# You can authenticate using one of the following methods: +# 1. API key (for local development) +GOOGLE_API_KEY=your_google_api_key_here +# 2. Service account key file (for production) +# Set this environment variable to the path of your service account key file +# GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/service-account-key.json +# 3. Application Default Credentials (ADC) +# No environment variables needed if using ADC +# See: https://cloud.google.com/docs/authentication/application-default-credentials # Function configuration # Set to 'true' to enable debug logging diff --git a/src/functions/prompts-to-test-spec/src/config.ts b/src/functions/prompts-to-test-spec/src/config.ts index 11c55d7..656db70 100644 --- a/src/functions/prompts-to-test-spec/src/config.ts +++ b/src/functions/prompts-to-test-spec/src/config.ts @@ -26,6 +26,7 @@ export const GITEA_PASSWORD = process.env.GITEA_PASSWORD; export const GOOGLE_CLOUD_PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID || ''; export const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; export const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-1.5-pro'; +export const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; // Function configuration export const DEBUG = process.env.DEBUG === 'true'; diff --git a/src/functions/prompts-to-test-spec/src/services/gemini-project-processor.ts b/src/functions/prompts-to-test-spec/src/services/gemini-project-processor.ts index 96c2925..35c300c 100644 --- a/src/functions/prompts-to-test-spec/src/services/gemini-project-processor.ts +++ b/src/functions/prompts-to-test-spec/src/services/gemini-project-processor.ts @@ -120,33 +120,12 @@ export class GeminiProjectProcessor { // Determine initial status based on workitem activity let status: 'skipped' | 'updated' | 'created' = 'skipped'; - // If workitem is not active, skip processing - if (!workitem.isActive) { - console.log(`GeminiProjectProcessor: Skipping inactive workitem: ${workitem.name}`); - - // If the feature file exists, it should be deleted - const featureFileName = `${workitem.name}.feature`; - const featurePath = path.join(this.projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems', featureFileName); - - if (fs.existsSync(featurePath)) { - fs.unlinkSync(featurePath); - console.log(`GeminiProjectProcessor: Deleted feature file for inactive workitem: ${featurePath}`); - } - - return { success: true, status: 'skipped', filesWritten: [] }; - } - // Read workitem content const workitemContent = fs.readFileSync(workitem.path, 'utf-8'); // Collect all relevant files from the project directory const relevantFiles = await this.collectRelevantFiles(workitem); - // Check if the feature file already exists to determine if this is an update or creation - const featureFileName = `${workitem.name}.feature`; - const featurePath = path.join(this.projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems', featureFileName); - status = fs.existsSync(featurePath) ? 'updated' : 'created'; - // Let Gemini decide what to do with the workitem const result = await this.generateFeatureFile( projectGuidelines, @@ -155,9 +134,6 @@ export class GeminiProjectProcessor { relevantFiles ); - // Gemini will handle the file operations through function calls - // No need to manually create or delete files here - // Get the list of files written for this workitem const filesWritten = this.filesWritten.get(workitem.name) || []; @@ -243,14 +219,6 @@ export class GeminiProjectProcessor { } } - // Check for existing feature file if it exists - const featureFileName = `${workitem.name}.feature`; - const featurePath = path.join(this.projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems', featureFileName); - - if (fs.existsSync(featurePath)) { - relevantFiles['existing_feature.feature'] = fs.readFileSync(featurePath, 'utf-8'); - } - console.log(`GeminiProjectProcessor: Collected ${Object.keys(relevantFiles).length} relevant files for workitem ${workitem.name}`); } catch (error) { console.error(`Error collecting relevant files for workitem ${workitem.name}:`, error); diff --git a/src/functions/prompts-to-test-spec/src/services/gemini-service.ts b/src/functions/prompts-to-test-spec/src/services/gemini-service.ts index 6be2de3..792a9ef 100644 --- a/src/functions/prompts-to-test-spec/src/services/gemini-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/gemini-service.ts @@ -9,7 +9,8 @@ import { GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, GEMINI_MODEL, - DRY_RUN_SKIP_GEMINI + DRY_RUN_SKIP_GEMINI, + GOOGLE_API_KEY } from '../config'; export class GeminiService { @@ -28,6 +29,12 @@ export class GeminiService { throw new Error('Google Cloud Project ID is required'); } + // Initialize VertexAI with default authentication + // Authentication is handled by the Google Auth library, which supports: + // 1. API keys via GOOGLE_API_KEY environment variable + // 2. Service accounts via GOOGLE_APPLICATION_CREDENTIALS environment variable + // 3. Application Default Credentials + // See: https://cloud.google.com/vertex-ai/docs/authentication this.vertexAI = new VertexAI({ project: this.projectId, location: this.location, @@ -134,86 +141,6 @@ export class GeminiService { ]; } - /** - * Apply project guidelines to a workitem - * @param project Project information - * @param workitem Workitem to process - * @param projectRepoPath Path to the cloned project repository - * @returns Result of the processing - */ - async processWorkitem( - project: Project, - workitem: Workitem, - projectRepoPath: string - ): Promise<{ success: boolean; error?: string }> { - try { - // Skip inactive workitems - if (!workitem.isActive) { - console.log(`Skipping inactive workitem: ${workitem.name}`); - - // If the feature file exists, it should be deleted according to guidelines - const featureFileName = `${workitem.name}.feature`; - const featurePath = path.join(projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems', featureFileName); - - if (fs.existsSync(featurePath)) { - fs.unlinkSync(featurePath); - console.log(`Deleted feature file for inactive workitem: ${featurePath}`); - } - - return { success: true }; - } - - // Read project guidelines - const projectGuidelines = await this.readProjectGuidelines(project.path); - - // Read workitem content - const workitemContent = fs.readFileSync(workitem.path, 'utf-8'); - - // Generate feature file content using Gemini API - const featureContent = await this.generateFeatureFile( - projectGuidelines, - workitemContent, - workitem.name - ); - - // Ensure the target directory exists - const targetDir = path.join(projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems'); - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, { recursive: true }); - } - - // Write the feature file - const featureFileName = `${workitem.name}.feature`; - const featurePath = path.join(targetDir, featureFileName); - fs.writeFileSync(featurePath, featureContent); - - console.log(`Created/updated feature file: ${featurePath}`); - - return { success: true }; - } catch (error) { - console.error(`Error processing workitem ${workitem.name}:`, error); - return { - success: false, - error: error instanceof Error ? error.message : String(error) - }; - } - } - - /** - * Read AI guidelines for a project - * @param projectPath Path to the project directory - * @returns AI guidelines content - */ - private async readProjectGuidelines(projectPath: string): Promise { - const aiPath = path.join(projectPath, 'AI.md'); - - if (!fs.existsSync(aiPath)) { - return ''; - } - - return fs.readFileSync(aiPath, 'utf-8'); - } - /** * Generate feature file content using Gemini API * @param guidelines Project guidelines @@ -276,8 +203,9 @@ You have access to file operations to help you understand the project structure - grepFiles(searchString, filePattern): Search for a string in project files, optionally filtered by a file pattern - deleteFile(filePath): Delete a file from the project repository -You can decide whether to create, update, or skip implementing this workitem based on your analysis. +You can decide whether to create, update, delete or skip implementing this workitem based on your analysis. Include the decision in your response. + ${additionalContext ? `\nAdditional context from project files:${additionalContext}` : ''} `; @@ -286,9 +214,12 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte // Send the initial message const result = await chat.sendMessage(prompt); + console.log(`Gemini result for ${workitemName}`, JSON.stringify(result, null, 2)); + // Process function calls if needed let finalResponse = await this.processFunctionCalls(result, chat, geminiProjectProcessor); + console.log(`Gemini response for ${workitemName}: ${finalResponse}`, result.response.candidates[0]); return finalResponse; } @@ -304,7 +235,8 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte // Check if there are function calls in the response if (!result.functionCalls || result.functionCalls.length === 0 || !geminiProjectProcessor) { // No function calls, return the text response - return result.text(); + // Access text content from the response structure in @google-cloud/vertexai v0.5.0 + return result.candidates?.[0]?.content?.parts?.[0]?.text || ''; } console.log(`Processing ${result.functionCalls.length} function calls from Gemini`); @@ -369,57 +301,35 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte } // Return the final text response - return result.text(); + // Access text content from the response structure in @google-cloud/vertexai v0.5.0 + return result.candidates?.[0]?.content?.parts?.[0]?.text || ''; } /** * Generate a pull request description using Gemini API * @param processedWorkitems List of processed workitems + * @param gitPatch Optional git patch showing code changes * @returns Generated pull request description */ async generatePullRequestDescription( - processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[] + processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[], + gitPatch?: string ): Promise { // Prepare workitem data for the prompt - const added: string[] = []; - const updated: string[] = []; - const deleted: string[] = []; - const failed: string[] = []; + const all: string[] = []; for (const item of processedWorkitems) { const { workitem, success, error } = item; - if (!success) { - failed.push(`- ${workitem.name}: ${error}`); - continue; - } - - if (!workitem.isActive) { - deleted.push(`- ${workitem.name}`); - } else if (workitem.implementation) { - updated.push(`- ${workitem.name}`); - } else { - added.push(`- ${workitem.name}`); - } + // Add all workitems to the all list regardless of status + all.push(`- ${workitem.name}`); } // Create a structured summary of changes let workitemSummary = ''; - if (added.length > 0) { - workitemSummary += 'Added workitems:\n' + added.join('\n') + '\n\n'; - } - - if (updated.length > 0) { - workitemSummary += 'Updated workitems:\n' + updated.join('\n') + '\n\n'; - } - - if (deleted.length > 0) { - workitemSummary += 'Deleted workitems:\n' + deleted.join('\n') + '\n\n'; - } - - if (failed.length > 0) { - workitemSummary += 'Failed workitems:\n' + failed.join('\n') + '\n\n'; + if (all.length > 0) { + workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n'; } // If dry run is enabled, return a mock PR description @@ -440,17 +350,32 @@ ${workitemSummary} model: this.model, }); + // Prepare the git patch section if available + let gitPatchSection = ''; + if (gitPatch && gitPatch !== "No changes detected.") { + gitPatchSection = ` +## Code Changes +The following code changes were made in this pull request: + +\`\`\`diff +${gitPatch} +\`\`\` +`; + } + const prompt = ` You are tasked with creating a pull request description for changes to test specifications. The following is a summary of the changes made: ${workitemSummary} +${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''} Create a clear, professional pull request description that: 1. Explains that this PR was automatically generated by the prompts-to-test-spec function 2. Summarizes the changes (added, updated, deleted, and failed workitems) -3. Uses markdown formatting for better readability -4. Keeps the description concise but informative +3. If code changes are provided, analyze them and include a summary of the key changes +4. Uses markdown formatting for better readability +5. Keeps the description concise but informative The pull request description should be ready to use without further editing. `; diff --git a/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts b/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts index 824ddc2..a16f1ee 100644 --- a/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts @@ -158,14 +158,8 @@ export class PullRequestService { processedWorkitems: { workitem: Workitem; success: boolean; error?: string; status?: 'skipped' | 'updated' | 'created'; filesWritten?: string[] }[], gitPatch?: string ): Promise { - // Use Gemini to generate the pull request description - const description = await this.geminiService.generatePullRequestDescription(processedWorkitems); - - // If there's a git patch, append it to the description - if (gitPatch && gitPatch !== "No changes detected.") { - return `${description}\n\n## Git Patch\n\`\`\`diff\n${gitPatch}\n\`\`\``; - } - - return description; + // Use Gemini to generate the pull request description, passing the git patch + // so Gemini can analyze the code changes + return await this.geminiService.generatePullRequestDescription(processedWorkitems, gitPatch); } }