WIP
This commit is contained in:
parent
b207103030
commit
30a44455e8
@ -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
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
`;
|
`;
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user