cghislai d6ddd8aa45 WIP
2025-06-08 01:20:00 +02:00

230 lines
6.9 KiB
TypeScript

/**
* Service for handling Gemini API operations
*/
import { VertexAI } from '@google-cloud/vertexai';
import * as fs from 'fs';
import * as path from 'path';
import { Project, Workitem } from '../types';
import { GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, GEMINI_MODEL } from '../config';
export class GeminiService {
private vertexAI: VertexAI;
private model: string;
private projectId: string;
private location: string;
constructor(projectId?: string, location?: string, model?: string) {
this.projectId = projectId || GOOGLE_CLOUD_PROJECT_ID;
this.location = location || GOOGLE_CLOUD_LOCATION;
this.model = model || GEMINI_MODEL;
if (!this.projectId) {
throw new Error('Google Cloud Project ID is required');
}
this.vertexAI = new VertexAI({
project: this.projectId,
location: this.location,
});
}
/**
* 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
* @param guidelines Project guidelines
* @param workitemContent Workitem content
* @param workitemName Name of the workitem
* @returns Generated feature file content
*/
private async generateFeatureFile(
guidelines: string,
workitemContent: string,
workitemName: string
): Promise<string> {
const generativeModel = this.vertexAI.getGenerativeModel({
model: this.model,
});
const currentDate = new Date().toISOString();
// Send the AI.md file directly to Gemini without hardcoded instructions
const prompt = `
${guidelines}
Workitem:
${workitemContent}
Include the following comment at the top of the generated file:
# Generated by prompts-to-test-spec on ${currentDate}
# Source: ${workitemName}
`;
const result = await generativeModel.generateContent({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
});
const response = await result.response;
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
return generatedText;
}
/**
* Generate a pull request description using Gemini API
* @param processedWorkitems List of processed workitems
* @returns Generated pull request description
*/
async generatePullRequestDescription(
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[]
): Promise<string> {
const generativeModel = this.vertexAI.getGenerativeModel({
model: this.model,
});
// Prepare workitem data for the prompt
const added: string[] = [];
const updated: string[] = [];
const deleted: string[] = [];
const failed: 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}`);
}
}
// Create a structured summary of changes for Gemini
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';
}
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}
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
The pull request description should be ready to use without further editing.
`;
const result = await generativeModel.generateContent({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
});
const response = await result.response;
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
return generatedText;
}
}