This commit is contained in:
cghislai 2025-06-08 04:18:09 +02:00
parent b207103030
commit 30a44455e8
5 changed files with 58 additions and 159 deletions

View File

@ -20,7 +20,18 @@ GITEA_PASSWORD=your_gitea_password_here
# Google Cloud configuration # Google Cloud configuration
GOOGLE_CLOUD_PROJECT_ID=your_gcp_project_id_here GOOGLE_CLOUD_PROJECT_ID=your_gcp_project_id_here
GOOGLE_CLOUD_LOCATION=us-central1 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 # Function configuration
# Set to 'true' to enable debug logging # Set to 'true' to enable debug logging

View File

@ -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_PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID || '';
export const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; 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 GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-1.5-pro';
export const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
// Function configuration // Function configuration
export const DEBUG = process.env.DEBUG === 'true'; export const DEBUG = process.env.DEBUG === 'true';

View File

@ -120,33 +120,12 @@ export class GeminiProjectProcessor {
// Determine initial status based on workitem activity // Determine initial status based on workitem activity
let status: 'skipped' | 'updated' | 'created' = 'skipped'; 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 // Read workitem content
const workitemContent = fs.readFileSync(workitem.path, 'utf-8'); const workitemContent = fs.readFileSync(workitem.path, 'utf-8');
// Collect all relevant files from the project directory // Collect all relevant files from the project directory
const relevantFiles = await this.collectRelevantFiles(workitem); 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 // Let Gemini decide what to do with the workitem
const result = await this.generateFeatureFile( const result = await this.generateFeatureFile(
projectGuidelines, projectGuidelines,
@ -155,9 +134,6 @@ export class GeminiProjectProcessor {
relevantFiles 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 // Get the list of files written for this workitem
const filesWritten = this.filesWritten.get(workitem.name) || []; 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}`); console.log(`GeminiProjectProcessor: Collected ${Object.keys(relevantFiles).length} relevant files for workitem ${workitem.name}`);
} catch (error) { } catch (error) {
console.error(`Error collecting relevant files for workitem ${workitem.name}:`, error); console.error(`Error collecting relevant files for workitem ${workitem.name}:`, error);

View File

@ -9,7 +9,8 @@ import {
GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_LOCATION,
GEMINI_MODEL, GEMINI_MODEL,
DRY_RUN_SKIP_GEMINI DRY_RUN_SKIP_GEMINI,
GOOGLE_API_KEY
} from '../config'; } from '../config';
export class GeminiService { export class GeminiService {
@ -28,6 +29,12 @@ export class GeminiService {
throw new Error('Google Cloud Project ID is required'); 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({ this.vertexAI = new VertexAI({
project: this.projectId, project: this.projectId,
location: this.location, 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<string> {
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 * Generate feature file content using Gemini API
* @param guidelines Project guidelines * @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 - 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 - 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. Include the decision in your response.
${additionalContext ? `\nAdditional context from project files:${additionalContext}` : ''} ${additionalContext ? `\nAdditional context from project files:${additionalContext}` : ''}
`; `;
@ -286,9 +214,12 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
// Send the initial message // Send the initial message
const result = await chat.sendMessage(prompt); const result = await chat.sendMessage(prompt);
console.log(`Gemini result for ${workitemName}`, JSON.stringify(result, null, 2));
// Process function calls if needed // Process function calls if needed
let finalResponse = await this.processFunctionCalls(result, chat, geminiProjectProcessor); let finalResponse = await this.processFunctionCalls(result, chat, geminiProjectProcessor);
console.log(`Gemini response for ${workitemName}: ${finalResponse}`, result.response.candidates[0]);
return finalResponse; return finalResponse;
} }
@ -304,7 +235,8 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
// Check if there are function calls in the response // Check if there are function calls in the response
if (!result.functionCalls || result.functionCalls.length === 0 || !geminiProjectProcessor) { if (!result.functionCalls || result.functionCalls.length === 0 || !geminiProjectProcessor) {
// No function calls, return the text response // 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`); 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 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 * Generate a pull request description using Gemini API
* @param processedWorkitems List of processed workitems * @param processedWorkitems List of processed workitems
* @param gitPatch Optional git patch showing code changes
* @returns Generated pull request description * @returns Generated pull request description
*/ */
async generatePullRequestDescription( async generatePullRequestDescription(
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[] processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
gitPatch?: string
): Promise<string> { ): Promise<string> {
// Prepare workitem data for the prompt // Prepare workitem data for the prompt
const added: string[] = []; const all: string[] = [];
const updated: string[] = [];
const deleted: string[] = [];
const failed: string[] = [];
for (const item of processedWorkitems) { for (const item of processedWorkitems) {
const { workitem, success, error } = item; const { workitem, success, error } = item;
if (!success) { // Add all workitems to the all list regardless of status
failed.push(`- ${workitem.name}: ${error}`); all.push(`- ${workitem.name}`);
continue;
}
if (!workitem.isActive) {
deleted.push(`- ${workitem.name}`);
} else if (workitem.implementation) {
updated.push(`- ${workitem.name}`);
} else {
added.push(`- ${workitem.name}`);
}
} }
// Create a structured summary of changes // Create a structured summary of changes
let workitemSummary = ''; let workitemSummary = '';
if (added.length > 0) { if (all.length > 0) {
workitemSummary += 'Added workitems:\n' + added.join('\n') + '\n\n'; workitemSummary += 'All workitems:\n' + all.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 dry run is enabled, return a mock PR description // If dry run is enabled, return a mock PR description
@ -440,17 +350,32 @@ ${workitemSummary}
model: this.model, 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 = ` const prompt = `
You are tasked with creating a pull request description for changes to test specifications. You are tasked with creating a pull request description for changes to test specifications.
The following is a summary of the changes made: The following is a summary of the changes made:
${workitemSummary} ${workitemSummary}
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
Create a clear, professional pull request description that: Create a clear, professional pull request description that:
1. Explains that this PR was automatically generated by the prompts-to-test-spec function 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) 2. Summarizes the changes (added, updated, deleted, and failed workitems)
3. Uses markdown formatting for better readability 3. If code changes are provided, analyze them and include a summary of the key changes
4. Keeps the description concise but informative 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. The pull request description should be ready to use without further editing.
`; `;

View File

@ -158,14 +158,8 @@ export class PullRequestService {
processedWorkitems: { workitem: Workitem; success: boolean; error?: string; status?: 'skipped' | 'updated' | 'created'; filesWritten?: string[] }[], processedWorkitems: { workitem: Workitem; success: boolean; error?: string; status?: 'skipped' | 'updated' | 'created'; filesWritten?: string[] }[],
gitPatch?: string gitPatch?: string
): Promise<string> { ): Promise<string> {
// Use Gemini to generate the pull request description // Use Gemini to generate the pull request description, passing the git patch
const description = await this.geminiService.generatePullRequestDescription(processedWorkitems); // so Gemini can analyze the code changes
return await this.geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
// 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;
} }
} }