This commit is contained in:
cghislai 2025-06-08 06:36:48 +02:00
parent c160d9bc5e
commit 9e4b0c2abb
8 changed files with 571 additions and 281 deletions

View File

@ -1,262 +0,0 @@
/**
* Service for handling Gemini API operations
*/
import {
FunctionDeclarationSchemaType,
GenerateContentCandidate,
GenerateContentRequest,
Tool,
VertexAI
} from '@google-cloud/vertexai';
import {Workitem} from '../types';
import {DRY_RUN_SKIP_GEMINI, GEMINI_MODEL, GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_PROJECT_ID} from '../config';
import {GeminiProjectProcessor} from './gemini-project-processor';
/**
* Interface for the model response format
*/
interface ModelResponse {
decision: 'create' | 'update' | 'delete' | 'skip';
reason: string;
}
/**
* Interface for the result returned by generateFeatureFile
*/
interface GenerateFeatureFileResult {
text: string;
decision?: ModelResponse;
}
/**
* Interface for function arguments
*/
interface FunctionArgs {
filePath?: string;
content?: string;
dirPath?: string;
searchString?: string;
filePattern?: string;
}
export class GeminiService {
private vertexAI: VertexAI;
private model: string;
private projectId: string;
private location: string;
private fileOperationTools: Tool[];
/**
* Create a new GeminiService instance
* @param projectId Google Cloud project ID (defaults to GOOGLE_CLOUD_PROJECT_ID from config)
* @param location Google Cloud location (defaults to GOOGLE_CLOUD_LOCATION from config)
* @param model Gemini model to use (defaults to GEMINI_MODEL from config)
*/
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');
}
// 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,
});
// Define file operation functions
this.fileOperationTools = [
{
function_declarations: [
{
name: "getFileContent",
description: "Get the content of a file in the project repository",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
filePath: {
type: FunctionDeclarationSchemaType.STRING,
description: "Path to the file relative to the project repository root"
}
},
required: ["filePath"]
}
},
{
name: "writeFileContent",
description: "Write content to a file in the project repository",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
filePath: {
type: FunctionDeclarationSchemaType.STRING,
description: "Path to the file relative to the project repository root"
},
content: {
type: FunctionDeclarationSchemaType.STRING,
description: "Content to write to the file"
}
},
required: ["filePath", "content"]
}
},
{
name: "fileExists",
description: "Check if a file exists in the project repository",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
filePath: {
type: FunctionDeclarationSchemaType.STRING,
description: "Path to the file relative to the project repository root"
}
},
required: ["filePath"]
}
},
{
name: "listFiles",
description: "List files in a directory in the project repository",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
dirPath: {
type: FunctionDeclarationSchemaType.STRING,
description: "Path to the directory relative to the project repository root"
}
},
required: ["dirPath"]
}
},
{
name: "grepFiles",
description: "Search for a string in project files",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
searchString: {
type: FunctionDeclarationSchemaType.STRING,
description: "String to search for in project files"
},
filePattern: {
type: FunctionDeclarationSchemaType.STRING,
description: "Optional file pattern to limit the search (e.g., '*.ts', 'src/*.java')"
}
},
required: ["searchString"]
}
},
{
name: "deleteFile",
description: "Delete a file from the project repository",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
filePath: {
type: FunctionDeclarationSchemaType.STRING,
description: "Path to the file relative to the project repository root"
}
},
required: ["filePath"]
}
}
]
}
];
}
/**
* Generate a pull request description using Gemini API
* @param processedWorkitems List of processed workitems with their status
* @param gitPatch Optional git patch showing code changes
* @returns Generated pull request description in markdown format
* @example
* const geminiService = new GeminiService();
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
*/
async generatePullRequestDescription(
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
gitPatch?: string
): Promise<string> {
// Prepare workitem data for the prompt
const all: string[] = [];
for (const item of processedWorkitems) {
const {workitem, success, error} = item;
// Add all workitems to the all list regardless of status
all.push(`- ${workitem.name}`);
}
// Create a structured summary of changes
let workitemSummary = '';
if (all.length > 0) {
workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n';
}
// If dry run is enabled, return a mock PR description
if (DRY_RUN_SKIP_GEMINI) {
console.log(`[DRY RUN] Skipping Gemini API call for generating pull request description`);
return `# Automated PR: Update Workitems (DRY RUN)
This pull request was automatically generated by the prompts-to-test-spec function in dry run mode.
## Changes Summary
${workitemSummary}
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
}
const generativeModel = this.vertexAI.getGenerativeModel({
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. 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.
`;
const result = await generativeModel.generateContent(prompt);
const response = await result.response;
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
return generatedText;
}
}

View File

@ -238,18 +238,7 @@ export class ModelStreamService {
// If dry run is enabled, return a mock result // If dry run is enabled, return a mock result
if (this.dryRun || DRY_RUN_SKIP_GEMINI) { if (this.dryRun || DRY_RUN_SKIP_GEMINI) {
console.log(`[DRY RUN] Skipping Gemini API call for processing workitem ${this.workitem.name}`); console.log(`[DRY RUN] Skipping Gemini API call for processing workitem ${this.workitem.name}`);
const mockText = `# Generated by prompts-to-test-spec on ${currentDate} (DRY RUN) const mockText = `# Generated by prompts-to-test-spec on ${currentDate} (DRY RUN)`;
# Source: ${this.workitem.name}
Feature: ${this.workitem.name} (DRY RUN)
This is a mock feature file generated during dry run.
No actual Gemini API call was made.
Scenario: Mock scenario
Given a dry run is enabled
When the feature file is generated
Then a mock feature file is returned
`;
return { return {
text: mockText, text: mockText,
decision: { decision: {

View File

@ -1,8 +1,8 @@
/** /**
* Service for handling pull request operations * Service for handling pull request operations
*/ */
import {PullRequestService as SharedPullRequestService, Project, RepoCredentials, Workitem} from 'shared-functions'; import {PullRequestService as SharedPullRequestService, Project, RepoCredentials, Workitem, GeminiService} from 'shared-functions';
import {GeminiService} from './gemini-service'; import {GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, GEMINI_MODEL, DRY_RUN_SKIP_GEMINI} from '../config';
export class PullRequestService { export class PullRequestService {
private sharedPullRequestService: SharedPullRequestService; private sharedPullRequestService: SharedPullRequestService;
@ -10,7 +10,12 @@ export class PullRequestService {
constructor() { constructor() {
this.sharedPullRequestService = new SharedPullRequestService(); this.sharedPullRequestService = new SharedPullRequestService();
this.geminiService = new GeminiService(); this.geminiService = new GeminiService(
GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION,
GEMINI_MODEL,
DRY_RUN_SKIP_GEMINI
);
} }
/** /**

View File

@ -8,6 +8,7 @@
"name": "shared-functions", "name": "shared-functions",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@google-cloud/vertexai": "^0.5.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"simple-git": "^3.20.0" "simple-git": "^3.20.0"
}, },
@ -596,6 +597,18 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@google-cloud/vertexai": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-0.5.0.tgz",
"integrity": "sha512-qIFHYTXA5UCLdm9JG+Xf1suomCXxRqa1PKdYjqXuhZsCm8mn37Rb0Tf8djlhDzuRVWyWoQTmsWpsk28ZTmbqJg==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.1.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -1333,6 +1346,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -1573,6 +1595,35 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1653,6 +1704,12 @@
"node-int64": "^0.4.0" "node-int64": "^0.4.0"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -1985,6 +2042,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ejs": { "node_modules/ejs": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -2321,6 +2387,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2527,6 +2599,36 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -2658,6 +2760,32 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -2684,6 +2812,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -2740,6 +2881,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -2916,7 +3070,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -3664,6 +3817,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -3705,6 +3867,27 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3929,6 +4112,26 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-int64": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@ -4459,6 +4662,26 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -4725,6 +4948,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-jest": { "node_modules/ts-jest": {
"version": "29.3.4", "version": "29.3.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
@ -4899,6 +5128,19 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@ -4924,6 +5166,22 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -10,6 +10,7 @@
"lint": "eslint src/**/*.ts" "lint": "eslint src/**/*.ts"
}, },
"dependencies": { "dependencies": {
"@google-cloud/vertexai": "^0.5.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"simple-git": "^3.20.0" "simple-git": "^3.20.0"
}, },

View File

@ -9,3 +9,4 @@ export * from './types';
export { ProjectService } from './services/project-service'; export { ProjectService } from './services/project-service';
export { RepositoryService } from './services/repository-service'; export { RepositoryService } from './services/repository-service';
export { PullRequestService } from './services/pull-request-service'; export { PullRequestService } from './services/pull-request-service';
export { GeminiService } from './services/gemini-service';

View File

@ -0,0 +1,162 @@
/**
* Tests for the GeminiService
*/
import { GeminiService } from '../gemini-service';
import { Workitem } from '../../types';
import { VertexAI } from '@google-cloud/vertexai';
// Mock VertexAI
jest.mock('@google-cloud/vertexai', () => {
return {
VertexAI: jest.fn().mockImplementation(() => {
return {
getGenerativeModel: jest.fn().mockImplementation(() => {
return {
generateContent: jest.fn().mockResolvedValue({
response: {
candidates: [
{
content: {
parts: [
{
text: '# Generated PR Description\n\nThis is a test PR description.'
}
]
}
}
]
}
})
};
})
};
})
};
});
describe('GeminiService', () => {
let geminiService: GeminiService;
beforeEach(() => {
jest.clearAllMocks();
geminiService = new GeminiService('test-project-id');
});
describe('constructor', () => {
it('should initialize with default values', () => {
const service = new GeminiService('test-project-id');
expect(VertexAI).toHaveBeenCalledWith({
project: 'test-project-id',
location: 'us-central1'
});
});
it('should initialize with custom values', () => {
const service = new GeminiService('test-project-id', 'europe-west1', 'gemini-1.0-pro');
expect(VertexAI).toHaveBeenCalledWith({
project: 'test-project-id',
location: 'europe-west1'
});
});
it('should throw an error if project ID is not provided', () => {
expect(() => new GeminiService('')).toThrow('Google Cloud Project ID is required');
});
});
describe('generatePullRequestDescription', () => {
it('should generate a PR description', async () => {
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
{
workitem: {
name: 'test-workitem',
path: '/path/to/workitem',
title: 'Test Workitem',
description: 'This is a test workitem',
isActive: true
},
success: true
}
];
const result = await geminiService.generatePullRequestDescription(workitems);
expect(result).toBe('# Generated PR Description\n\nThis is a test PR description.');
});
it('should return a mock PR description in dry run mode', async () => {
const dryRunService = new GeminiService('test-project-id', 'us-central1', 'gemini-1.5-pro', true);
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
{
workitem: {
name: 'test-workitem',
path: '/path/to/workitem',
title: 'Test Workitem',
description: 'This is a test workitem',
isActive: true
},
success: true
}
];
const result = await dryRunService.generatePullRequestDescription(workitems);
expect(result).toContain('# Automated PR: Update Workitems (DRY RUN)');
expect(result).toContain('All workitems:');
expect(result).toContain('- test-workitem');
expect(result).toContain('*Note: This is a mock PR description generated during dry run.');
});
it('should include git patch in the prompt if provided', async () => {
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
{
workitem: {
name: 'test-workitem',
path: '/path/to/workitem',
title: 'Test Workitem',
description: 'This is a test workitem',
isActive: true
},
success: true
}
];
const gitPatch = 'diff --git a/file.txt b/file.txt\nindex 1234..5678 100644\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n-old line\n+new line';
const mockGenerateContent = jest.fn().mockResolvedValue({
response: {
candidates: [
{
content: {
parts: [
{
text: '# Generated PR Description\n\nThis is a test PR description with git patch.'
}
]
}
}
]
}
});
const mockGetGenerativeModel = jest.fn().mockReturnValue({
generateContent: mockGenerateContent
});
(VertexAI as jest.Mock).mockImplementationOnce(() => ({
getGenerativeModel: mockGetGenerativeModel
}));
const service = new GeminiService('test-project-id');
const result = await service.generatePullRequestDescription(workitems, gitPatch);
expect(mockGenerateContent).toHaveBeenCalled();
const promptArg = mockGenerateContent.mock.calls[0][0];
expect(promptArg).toContain('Code Changes');
expect(promptArg).toContain('```diff');
expect(promptArg).toContain(gitPatch);
expect(result).toBe('# Generated PR Description\n\nThis is a test PR description with git patch.');
});
});
});

View File

@ -0,0 +1,136 @@
/**
* Service for handling Gemini API operations
*/
import {
GenerateContentCandidate,
VertexAI
} from '@google-cloud/vertexai';
import { Workitem } from '../types';
export class GeminiService {
private vertexAI: VertexAI;
private model: string;
private projectId: string;
private location: string;
private dryRunSkipGemini: boolean;
/**
* Create a new GeminiService instance
* @param projectId Google Cloud project ID
* @param location Google Cloud location
* @param model Gemini model to use
* @param dryRunSkipGemini Whether to skip Gemini API calls in dry run mode
*/
constructor(
projectId: string,
location: string = 'us-central1',
model: string = 'gemini-1.5-pro',
dryRunSkipGemini: boolean = false
) {
this.projectId = projectId;
this.location = location;
this.model = model;
this.dryRunSkipGemini = dryRunSkipGemini;
if (!this.projectId) {
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,
});
}
/**
* Generate a pull request description using Gemini API
* @param processedWorkitems List of processed workitems with their status
* @param gitPatch Optional git patch showing code changes
* @returns Generated pull request description in markdown format
* @example
* const geminiService = new GeminiService('my-project-id');
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
*/
async generatePullRequestDescription(
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
gitPatch?: string
): Promise<string> {
// Prepare workitem data for the prompt
const all: string[] = [];
for (const item of processedWorkitems) {
const {workitem, success, error} = item;
// Add all workitems to the all list regardless of status
all.push(`- ${workitem.name}`);
}
// Create a structured summary of changes
let workitemSummary = '';
if (all.length > 0) {
workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n';
}
// If dry run is enabled, return a mock PR description
if (this.dryRunSkipGemini) {
console.log(`[DRY RUN] Skipping Gemini API call for generating pull request description`);
return `# Automated PR: Update Workitems (DRY RUN)
This pull request was automatically generated in dry run mode.
## Changes Summary
${workitemSummary}
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
}
const generativeModel = this.vertexAI.getGenerativeModel({
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
2. Summarizes the changes (added, updated, deleted, and failed workitems)
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.
`;
const result = await generativeModel.generateContent(prompt);
const response = await result.response;
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
return generatedText;
}
}