This commit is contained in:
cghislai 2025-06-08 04:40:26 +02:00
parent 30a44455e8
commit dd4b116bbb

View File

@ -1,10 +1,16 @@
/** /**
* Service for handling Gemini API operations * Service for handling Gemini API operations
*/ */
import { VertexAI, FunctionDeclaration, Tool, FunctionDeclarationSchemaType } from '@google-cloud/vertexai'; import {
VertexAI,
FunctionDeclaration,
Tool,
FunctionDeclarationSchemaType,
GenerateContentRequest
} from '@google-cloud/vertexai';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { Project, Workitem } from '../types'; import {Project, Workitem} from '../types';
import { import {
GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_LOCATION,
@ -142,7 +148,7 @@ export class GeminiService {
} }
/** /**
* Generate feature file content using Gemini API * Generate feature file content using Gemini API with streaming
* @param guidelines Project guidelines * @param guidelines Project guidelines
* @param workitemContent Workitem content * @param workitemContent Workitem content
* @param workitemName Name of the workitem * @param workitemName Name of the workitem
@ -176,12 +182,7 @@ Feature: ${workitemName} (DRY RUN)
`; `;
} }
const generativeModel = this.vertexAI.getGenerativeModel({ // Create the prompt
model: this.model,
tools: geminiProjectProcessor ? this.fileOperationTools : undefined,
});
// Send the AI.md file and additional context to Gemini without hardcoded instructions
const prompt = ` const prompt = `
${guidelines} ${guidelines}
@ -195,7 +196,7 @@ Include the following comment at the top of any generated files:
# Generated by prompts-to-test-spec on ${currentDate} # Generated by prompts-to-test-spec on ${currentDate}
# Source: ${workitemName} # Source: ${workitemName}
You have access to file operations to help you understand the project structure and create better implementations: You have access to the following function calls to help you understand the project structure and create implementations:
- getFileContent(filePath): Get the content of a file in the project repository - getFileContent(filePath): Get the content of a file in the project repository
- writeFileContent(filePath, content): Write content to a file in the project repository - writeFileContent(filePath, content): Write content to a file in the project repository
- fileExists(filePath): Check if a file exists in the project repository - fileExists(filePath): Check if a file exists in the project repository
@ -204,23 +205,223 @@ You have access to file operations to help you understand the project structure
- 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, delete 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.
In your response, just include your decision with a short motivation in json format. For instance:
{ "decision": "create", "reason": "This workitem was not implemented" }
${additionalContext ? `\nAdditional context from project files:${additionalContext}` : ''} ${additionalContext ? `\nAdditional context from project files:${additionalContext}` : ''}
`; `;
// Start the chat session // Initialize Vertex with your Cloud project and location
const chat = generativeModel.startChat(); const vertexAI = new VertexAI({
project: this.projectId,
location: this.location,
});
// Send the initial message // Instantiate the model with our file operation tools
const result = await chat.sendMessage(prompt); const generativeModel = vertexAI.getGenerativeModel({
console.log(`Gemini result for ${workitemName}`, JSON.stringify(result, null, 2)); model: this.model,
tools: geminiProjectProcessor ? this.fileOperationTools : undefined,
generation_config: {
temperature: 0.1, // Very low temperature for more deterministic responses
top_p: 0.95, // Higher top_p to allow more diverse completions when needed
top_k: 40, // Consider only the top 40 tokens
},
});
// Create the initial request
const request: GenerateContentRequest = {
contents: [
{role: 'user', parts: [{text: prompt}]}
],
tools: geminiProjectProcessor ? this.fileOperationTools : undefined,
};
// Process function calls if needed // Generate content in a streaming fashion
let finalResponse = await this.processFunctionCalls(result, chat, geminiProjectProcessor); const streamingResp = await generativeModel.generateContentStream(request);
console.log(`Gemini response for ${workitemName}: ${finalResponse}`, result.response.candidates[0]);
let finalResponse = '';
let pendingFunctionCalls = [];
// Process the streaming response
for await (const item of streamingResp.stream) {
// Check if there's a function call in any part of the response
let functionCall = null;
let textContent = '';
// Iterate over every part in the response
for (const part of item.candidates?.[0]?.content?.parts || []) {
if (part.functionCall) {
functionCall = part.functionCall;
break;
} else if (part.text) {
textContent += part.text;
}
}
if (functionCall) {
console.log(`Function call detected: ${functionCall.name}`);
pendingFunctionCalls.push(functionCall);
} else if (textContent) {
// If there's text, append it to the final response
finalResponse += textContent;
}
}
// Process any function calls that were detected
if (pendingFunctionCalls.length > 0 && geminiProjectProcessor) {
console.log(`Processing ${pendingFunctionCalls.length} function calls from streaming response`);
let currentRequest: GenerateContentRequest = request;
// Process each function call
for (const functionCall of pendingFunctionCalls) {
const functionName = functionCall.name;
const functionArgs = (typeof functionCall.args === 'string' ?
JSON.parse(functionCall.args) : functionCall.args) as {
filePath?: string;
content?: string;
dirPath?: string;
searchString?: string;
filePattern?: string;
};
console.log(`Executing function: ${functionName} with args:`, functionArgs);
let functionResponse;
try {
// Execute the function using the GeminiProjectProcessor
switch (functionName) {
case 'getFileContent':
functionResponse = geminiProjectProcessor.getFileContent(functionArgs.filePath!);
break;
case 'writeFileContent':
// Get the current workitem name from the context
const currentWorkitem = geminiProjectProcessor.getCurrentWorkitem();
geminiProjectProcessor.writeFileContent(functionArgs.filePath!, functionArgs.content!, currentWorkitem?.name);
functionResponse = `File ${functionArgs.filePath} written successfully`;
break;
case 'fileExists':
functionResponse = geminiProjectProcessor.fileExists(functionArgs.filePath!);
break;
case 'listFiles':
functionResponse = geminiProjectProcessor.listFiles(functionArgs.dirPath!);
break;
case 'grepFiles':
functionResponse = geminiProjectProcessor.grepFiles(functionArgs.searchString!, functionArgs.filePattern);
break;
case 'deleteFile':
functionResponse = geminiProjectProcessor.deleteFile(functionArgs.filePath!);
break;
default:
throw new Error(`Unknown function: ${functionName}`);
}
// Create a function response object
const functionResponseObj = {
name: functionName,
response: {result: JSON.stringify(functionResponse)}
};
// Update the request with the function call and response
currentRequest = {
contents: [
...currentRequest.contents,
{
role: 'ASSISTANT',
parts: [
{
functionCall: functionCall
}
]
},
{
role: 'USER',
parts: [
{
functionResponse: functionResponseObj
}
]
}
],
tools: geminiProjectProcessor ? this.fileOperationTools : undefined,
};
// Generate the next response
const nextStreamingResp = await generativeModel.generateContentStream(currentRequest);
// Process the next streaming response
for await (const nextItem of nextStreamingResp.stream) {
let textContent = '';
// Iterate over every part in the response
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
if (part.text) {
textContent += part.text;
}
}
if (textContent) {
finalResponse += textContent;
}
}
} catch (error) {
console.error(`Error executing function ${functionName}:`, error);
// Create an error response object
const errorResponseObj = {
name: functionName,
response: {error: error instanceof Error ? error.message : String(error)}
};
// Update the request with the function call and error response
currentRequest = {
contents: [
...currentRequest.contents,
{
role: 'ASSISTANT',
parts: [
{
functionCall: functionCall
}
]
},
{
role: 'USER',
parts: [
{
functionResponse: errorResponseObj
}
]
}
],
tools: geminiProjectProcessor ? this.fileOperationTools : undefined,
};
// Generate the next response
const nextStreamingResp = await generativeModel.generateContentStream(currentRequest);
// Process the next streaming response
for await (const nextItem of nextStreamingResp.stream) {
let textContent = '';
// Iterate over every part in the response
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
if (part.text) {
textContent += part.text;
}
}
if (textContent) {
finalResponse += textContent;
}
}
}
}
}
console.log(`Gemini response for ${workitemName}: ${finalResponse}`);
return finalResponse; return finalResponse;
} }
@ -233,18 +434,32 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
*/ */
private async processFunctionCalls(result: any, chat: any, geminiProjectProcessor?: any): Promise<string> { private async processFunctionCalls(result: any, chat: any, geminiProjectProcessor?: any): Promise<string> {
// 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) { // Function calls can be at the top level or nested within candidates
const functionCalls = result.functionCalls ||
(result.response?.candidates?.[0]?.functionCall ? [result.response.candidates[0].functionCall] : []) ||
(result.candidates?.[0]?.functionCall ? [result.candidates[0].functionCall] : []);
if (functionCalls.length === 0 || !geminiProjectProcessor) {
// No function calls, return the text response // No function calls, return the text response
// Access text content from the response structure in @google-cloud/vertexai v0.5.0 // Access text content from the response structure in @google-cloud/vertexai v0.5.0
return result.candidates?.[0]?.content?.parts?.[0]?.text || ''; return result.candidates?.[0]?.content?.parts?.[0]?.text ||
result.response?.candidates?.[0]?.content?.parts?.[0]?.text || '';
} }
console.log(`Processing ${result.functionCalls.length} function calls from Gemini`); console.log(`Processing ${functionCalls.length} function calls from Gemini`);
// Process each function call // Process each function call
for (const functionCall of result.functionCalls) { for (const functionCall of functionCalls) {
const functionName = functionCall.name; const functionName = functionCall.name;
const functionArgs = JSON.parse(functionCall.args); // Handle both cases: when args is already an object and when it's a string that needs to be parsed
const functionArgs = (typeof functionCall.args === 'string' ?
JSON.parse(functionCall.args) : functionCall.args) as {
filePath?: string;
content?: string;
dirPath?: string;
searchString?: string;
filePattern?: string;
};
console.log(`Executing function: ${functionName} with args:`, functionArgs); console.log(`Executing function: ${functionName} with args:`, functionArgs);
@ -253,32 +468,37 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
// Execute the function using the GeminiProjectProcessor // Execute the function using the GeminiProjectProcessor
switch (functionName) { switch (functionName) {
case 'getFileContent': case 'getFileContent':
functionResponse = geminiProjectProcessor.getFileContent(functionArgs.filePath); functionResponse = geminiProjectProcessor.getFileContent(functionArgs.filePath!);
break; break;
case 'writeFileContent': case 'writeFileContent':
// Get the current workitem name from the context // Get the current workitem name from the context
const currentWorkitem = geminiProjectProcessor.getCurrentWorkitem(); const currentWorkitem = geminiProjectProcessor.getCurrentWorkitem();
geminiProjectProcessor.writeFileContent(functionArgs.filePath, functionArgs.content, currentWorkitem?.name); geminiProjectProcessor.writeFileContent(functionArgs.filePath!, functionArgs.content!, currentWorkitem?.name);
functionResponse = `File ${functionArgs.filePath} written successfully`; functionResponse = `File ${functionArgs.filePath} written successfully`;
break; break;
case 'fileExists': case 'fileExists':
functionResponse = geminiProjectProcessor.fileExists(functionArgs.filePath); functionResponse = geminiProjectProcessor.fileExists(functionArgs.filePath!);
break; break;
case 'listFiles': case 'listFiles':
functionResponse = geminiProjectProcessor.listFiles(functionArgs.dirPath); functionResponse = geminiProjectProcessor.listFiles(functionArgs.dirPath!);
break; break;
case 'grepFiles': case 'grepFiles':
functionResponse = geminiProjectProcessor.grepFiles(functionArgs.searchString, functionArgs.filePattern); functionResponse = geminiProjectProcessor.grepFiles(functionArgs.searchString!, functionArgs.filePattern);
break; break;
case 'deleteFile': case 'deleteFile':
functionResponse = geminiProjectProcessor.deleteFile(functionArgs.filePath); functionResponse = geminiProjectProcessor.deleteFile(functionArgs.filePath!);
break; break;
default: default:
throw new Error(`Unknown function: ${functionName}`); throw new Error(`Unknown function: ${functionName}`);
} }
// Send the function response back to Gemini // Send the function response back to Gemini
const functionResponseObj = { functionResponse: { name: functionName, response: { result: JSON.stringify(functionResponse) } } }; const functionResponseObj = {
functionResponse: {
name: functionName,
response: {result: JSON.stringify(functionResponse)}
}
};
const nextResult = await chat.sendMessage(functionResponseObj); const nextResult = await chat.sendMessage(functionResponseObj);
// Recursively process any additional function calls // Recursively process any additional function calls
@ -290,7 +510,7 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
const errorResponseObj = { const errorResponseObj = {
functionResponse: { functionResponse: {
name: functionName, name: functionName,
response: { error: error instanceof Error ? error.message : String(error) } response: {error: error instanceof Error ? error.message : String(error)}
} }
}; };
const nextResult = await chat.sendMessage(errorResponseObj); const nextResult = await chat.sendMessage(errorResponseObj);
@ -305,6 +525,240 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
return result.candidates?.[0]?.content?.parts?.[0]?.text || ''; return result.candidates?.[0]?.content?.parts?.[0]?.text || '';
} }
/**
* Example of using function calling with streaming content generation
* This method demonstrates how to use the Vertex AI API for function calling in a streaming context
* @param projectId Google Cloud project ID
* @param location Google Cloud location
* @param model Gemini model to use
*/
async functionCallingStreamExample(
projectId: string = this.projectId,
location: string = this.location,
model: string = this.model
): Promise<void> {
// Initialize Vertex with your Cloud project and location
const vertexAI = new VertexAI({project: projectId, location: location});
// Instantiate the model
const generativeModel = vertexAI.getGenerativeModel({
model: model,
});
// Define the function declaration for the weather function
const functionDeclarations: FunctionDeclaration[] = [
{
name: "get_current_weather",
description: "Get the current weather in a given location",
parameters: {
type: FunctionDeclarationSchemaType.OBJECT,
properties: {
location: {
type: FunctionDeclarationSchemaType.STRING,
description: "The city and state, e.g., San Francisco, CA"
}
},
required: ["location"]
}
}
];
// Create a mock function response
const functionResponseParts = [
{
functionResponse: {
name: "get_current_weather",
response: {
temperature: "72",
unit: "fahrenheit",
description: "Sunny"
}
}
}
];
// Create the request with function calling
const request = {
contents: [
{role: 'user', parts: [{text: 'What is the weather in Boston?'}]},
{
role: 'ASSISTANT',
parts: [
{
functionCall: {
name: 'get_current_weather',
args: {location: 'Boston'},
},
},
],
},
{role: 'USER', parts: functionResponseParts},
],
tools: [{function_declarations: functionDeclarations}],
};
// Generate content in a streaming fashion
const streamingResp = await generativeModel.generateContentStream(request);
// Process the streaming response
for await (const item of streamingResp.stream) {
// Iterate over every part in the response
for (const part of item.candidates?.[0]?.content?.parts || []) {
if (part.text) {
console.log(part.text);
}
}
}
}
/**
* Example of using function calling with streaming content generation for file operations
* This method demonstrates how to use the Vertex AI API for file operation function calling in a streaming context
* @param workitemName Name of the workitem
* @param geminiProjectProcessor The GeminiProjectProcessor to handle function calls
*/
async fileOperationsStreamExample(
workitemName: string,
geminiProjectProcessor: any
): Promise<void> {
// Initialize Vertex with your Cloud project and location
const vertexAI = new VertexAI({
project: this.projectId,
location: this.location,
});
// Instantiate the model with our file operation tools
const generativeModel = vertexAI.getGenerativeModel({
model: this.model,
tools: this.fileOperationTools,
});
// Create a prompt that asks the model to check if a file exists and create it if it doesn't
const prompt = `Check if the file 'example.txt' exists and create it with some content if it doesn't.`;
// Create the initial request
const request = {
contents: [
{role: 'user', parts: [{text: prompt}]}
],
tools: this.fileOperationTools,
};
// Generate content in a streaming fashion
const streamingResp = await generativeModel.generateContentStream(request);
// Process the streaming response
for await (const item of streamingResp.stream) {
// Check if there's a function call in any part of the response
let functionCall = null;
// Iterate over every part in the response
for (const part of item.candidates?.[0]?.content?.parts || []) {
if (part.functionCall) {
functionCall = part.functionCall;
break;
}
}
if (functionCall) {
console.log(`Function call detected: ${functionCall.name}`);
// Execute the function
const functionName = functionCall.name;
const functionArgs = functionCall.args as {
filePath?: string;
content?: string;
dirPath?: string;
searchString?: string;
filePattern?: string;
};
console.log(`Executing function: ${functionName} with args:`, functionArgs);
let functionResponse;
try {
// Execute the function using the GeminiProjectProcessor
switch (functionName) {
case 'getFileContent':
functionResponse = geminiProjectProcessor.getFileContent(functionArgs.filePath!);
break;
case 'writeFileContent':
geminiProjectProcessor.writeFileContent(functionArgs.filePath!, functionArgs.content!, workitemName);
functionResponse = `File ${functionArgs.filePath} written successfully`;
break;
case 'fileExists':
functionResponse = geminiProjectProcessor.fileExists(functionArgs.filePath!);
break;
case 'listFiles':
functionResponse = geminiProjectProcessor.listFiles(functionArgs.dirPath!);
break;
case 'grepFiles':
functionResponse = geminiProjectProcessor.grepFiles(functionArgs.searchString!, functionArgs.filePattern);
break;
case 'deleteFile':
functionResponse = geminiProjectProcessor.deleteFile(functionArgs.filePath!);
break;
default:
throw new Error(`Unknown function: ${functionName}`);
}
// Create a new request with the function response
const functionResponseObj = {
name: functionName,
response: {result: JSON.stringify(functionResponse)}
};
// Continue the conversation with the function response
const nextRequest = {
contents: [
{role: 'user', parts: [{text: prompt}]},
{
role: 'ASSISTANT',
parts: [
{
functionCall: functionCall
}
]
},
{
role: 'USER',
parts: [
{
functionResponse: functionResponseObj
}
]
}
],
tools: this.fileOperationTools,
};
// Generate the next response
const nextStreamingResp = await generativeModel.generateContentStream(nextRequest);
// Process the next streaming response
for await (const nextItem of nextStreamingResp.stream) {
// Iterate over every part in the response
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
if (part.text) {
console.log(part.text);
}
}
}
} catch (error) {
console.error(`Error executing function ${functionName}:`, error);
}
} else {
// If there's no function call, just log the text from all parts
for (const part of item.candidates?.[0]?.content?.parts || []) {
if (part.text) {
console.log(part.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
@ -319,7 +773,7 @@ ${additionalContext ? `\nAdditional context from project files:${additionalConte
const all: string[] = []; const all: string[] = [];
for (const item of processedWorkitems) { for (const item of processedWorkitems) {
const { workitem, success, error } = item; const {workitem, success, error} = item;
// Add all workitems to the all list regardless of status // Add all workitems to the all list regardless of status
all.push(`- ${workitem.name}`); all.push(`- ${workitem.name}`);