WIP prompt engineering

This commit is contained in:
cghislai 2025-06-08 17:56:36 +02:00
parent 128ad5ee1f
commit 0bb5b9f876
7 changed files with 201 additions and 82 deletions

View File

@ -200,12 +200,12 @@ export class ProjectService {
let logMessage = `${timestamp} - Gemini updates`; let logMessage = `${timestamp} - Gemini updates`;
response.stepOutcomes.forEach(outcome => { response.stepOutcomes.forEach(outcome => {
logMessage += `\n- ${outcome.decision}: ${outcome.reason}`; logMessage += `\n- ${outcome.outcomes}: ${outcome.reason}`;
}) })
response.fileDeleted.forEach(file => { response.filesDeleted.forEach(file => {
logMessage += `\n- Delete file ${file}`; logMessage += `\n- Delete file ${file}`;
}) })
response.fileWritten.forEach(file => { response.filesWritten.forEach(file => {
logMessage += `\n- Added file ${file}`; logMessage += `\n- Added file ${file}`;
}) })

View File

@ -117,7 +117,7 @@ export class ProjectWorkitemsService {
relevantFiles relevantFiles
); );
const hasChanges = result.fileWritten.length > 0 || result.fileDeleted.length > 0; const hasChanges = result.filesWritten.length > 0 || result.filesDeleted.length > 0;
// Update the workitem file with implementation log // Update the workitem file with implementation log
if (hasChanges) { if (hasChanges) {
try { try {
@ -133,12 +133,12 @@ export class ProjectWorkitemsService {
} }
} }
console.log(`ProjectWorkitemsService: Completed processing workitem: ${workitem.name} (Files written: ${result.fileWritten.length})`); console.log(`ProjectWorkitemsService: Completed processing workitem: ${workitem.name} (Files written: ${result.filesWritten.length})`);
return { return {
success: true, success: true,
workitem, workitem,
filesWritten: result.fileWritten, filesWritten: result.filesWritten,
filesRemoved: result.fileDeleted, filesRemoved: result.filesDeleted,
}; };
} catch (error) { } catch (error) {
console.error(`Error processing workitem ${workitem.name}:`, error); console.error(`Error processing workitem ${workitem.name}:`, error);
@ -203,8 +203,8 @@ export class ProjectWorkitemsService {
if (DRY_RUN_SKIP_GEMINI) { if (DRY_RUN_SKIP_GEMINI) {
console.log(`[DRY RUN] Skipping Gemini API call for generating feature file for ${workitemName}`); console.log(`[DRY RUN] Skipping Gemini API call for generating feature file for ${workitemName}`);
return { return {
fileWritten: [], filesWritten: [],
fileDeleted: [], filesDeleted: [],
stepOutcomes: [], stepOutcomes: [],
modelResponses: [] modelResponses: []
}; };

View File

@ -23,18 +23,23 @@ export interface FunctionArgs {
dirPath?: string; dirPath?: string;
searchString?: string; searchString?: string;
filePattern?: string; filePattern?: string;
outcome?: 'create' | 'update' | 'delete' | 'skip'; step?: string;
reason?: string; outcome?: string | 'end';
description?: string;
} }
export interface GeminiResponse { export interface GeminiResponse {
fileWritten: string[]; filesWritten: string[];
fileDeleted: string[]; filesDeleted: string[];
stepOutcomes: { stepOutcomes: {
decision: 'create' | 'update' | 'delete' | 'skip'; step?: string;
outcomes: string;
reason: string; reason: string;
}[]; }[];
modelResponses: string[]; modelResponses: string[];
inputCost?: number;
outputCost?: number;
totalCost?: number;
} }
/** /**
@ -164,21 +169,25 @@ export class GeminiFileSystemService {
}, },
{ {
name: "reportStepOutcome", name: "reportStepOutcome",
description: "Submit the outcome for a step in compliance with guidelines. Can be called multiple times.", description: "Submit the status/outcome for a step in your workplan",
parameters: { parameters: {
type: FunctionDeclarationSchemaType.OBJECT, type: FunctionDeclarationSchemaType.OBJECT,
properties: { properties: {
step: {
type: FunctionDeclarationSchemaType.STRING,
description: "The step identifier",
},
outcome: { outcome: {
type: FunctionDeclarationSchemaType.STRING, type: FunctionDeclarationSchemaType.STRING,
description: "The step outcome: 'create', 'update', 'delete', or 'skip'", description: "The step outcome. Use the special value 'end' to abort/complete the session",
enum: ["create", "update", "delete", "skip"] enum: ["started", "done", "partially-done", "skip", "end", "end-confirmed"]
}, },
reason: { description: {
type: FunctionDeclarationSchemaType.STRING, type: FunctionDeclarationSchemaType.STRING,
description: "Reason for this outcome. For instance, 'create' when files have been created, 'skip' when no files has been created, or 'update' when files have been updated." description: "Description for this outcome. A short paragraph at most"
} }
}, },
required: ["outcome", "reason"] required: ["outcome", "description"]
} }
} }
] ]
@ -404,23 +413,26 @@ export class GeminiFileSystemService {
console.log(`[DRY RUN] Skipping Gemini API call for processing`); console.log(`[DRY RUN] Skipping Gemini API call for processing`);
return { return {
stepOutcomes: [], stepOutcomes: [],
fileDeleted: [], filesDeleted: [],
modelResponses: [], modelResponses: [],
fileWritten: [] filesWritten: []
}; };
} }
// Create the prompt // Create the prompt
const prompt = ` const prompts: string[] = [
Here is your guideline: `Here is your guidelines:
${guidelines}`,
${guidelines} `Additional content:
${additionalContent}`,
Additional content: `Make a work plan:
- create steps to comply with the guidelines
${additionalContent} - report each step outcome as you start them: use the reportStepOutcome(step, outcome, description) function
- start each step by considering creating substeps based on the outcome of the preceding steps
You have access to the following function calls to help you understand the project structure and create implementations: - keep track of step hierarchy by their identifiers. Dont create substeps at a depth higher than 5
- report each step outcome as you complete them: use the reportStepOutcome(step, outcome, description) function
`,
`Access the filesystem: You have access to the following function calls to interact with the project repository:
- 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 (create or update) - writeFileContent(filePath, content): Write content to a file in the project repository (create or update)
- fileExists(filePath): Check if a file exists in the project repository - fileExists(filePath): Check if a file exists in the project repository
@ -428,13 +440,17 @@ You have access to the following function calls to help you understand the proje
- grepFiles(searchString, filePattern): Search for a string in project files, optionally filtered by a file pattern (glob) - grepFiles(searchString, filePattern): Search for a string in project files, optionally filtered by a file pattern (glob)
use filePattern='path/**' to search recursively in all files under path. use filePattern='path/**' to search recursively in all files under path.
- deleteFile(filePath): Delete a file from the project repository - deleteFile(filePath): Delete a file from the project repository
`,
IMPORTANT: First use the function calls above to comply with the guidelines. Create, update, or delete all required files. `Be throughout:
Ensure each file you create is entirely implemented, and that you changes are fully compliant with the guidelines.
You can use this function to report the outcome of each step as you work through the guidelines: Create a new work list is additional scanning / editing is required.
- reportStepOutcome(outcome, reason): Outcome must be one of: 'create', 'update', 'delete', 'skip' `,
`Complete the session:
`; Once you have completed all steps, call reportStepOutcome with outcome 'end'`,
];
const promptContents: Content[] = prompts.map(promptPart => {
return {role: 'user', parts: [{text: promptPart}]}
})
// Instantiate the model with our file operation tools // Instantiate the model with our file operation tools
const generativeModel = this.vertexAI.getGenerativeModel({ const generativeModel = this.vertexAI.getGenerativeModel({
@ -447,9 +463,7 @@ You can use this function to report the outcome of each step as you work through
// Create the initial request // Create the initial request
const request: GenerateContentRequest = { const request: GenerateContentRequest = {
contents: [ contents: promptContents,
{role: 'user', parts: [{text: prompt}]}
],
tools: this.fileOperationTools, tools: this.fileOperationTools,
}; };
const geminiResponse = await this.handleGeminiStream(generativeModel, request, rootPath); const geminiResponse = await this.handleGeminiStream(generativeModel, request, rootPath);
@ -463,6 +477,22 @@ You can use this function to report the outcome of each step as you work through
return geminiResponse; return geminiResponse;
} }
private createReevaluationContrent(): Content [] {
return [
{
role: 'USER',
parts: [
{
text: `Re-evaluate compliance with all guidelines.
Create a new work list to comply if needed.
Report a step with outcome 'end-confirmed' and a description detailling your confidence if you are completely done`
}
]
}
];
}
private createFunctionExchangeContents( private createFunctionExchangeContents(
functionCall: FunctionCall, functionCall: FunctionCall,
responseData: any, responseData: any,
@ -497,7 +527,7 @@ You can use this function to report the outcome of each step as you work through
private processFunctionCall(functionCall: FunctionCall, rootPath: string, callbacks: { private processFunctionCall(functionCall: FunctionCall, rootPath: string, callbacks: {
onFileWritten: (file: string) => any; onFileWritten: (file: string) => any;
onFileDelete: (file: string) => any; onFileDelete: (file: string) => any;
onStepOutcome: (outcome: 'create' | 'update' | 'delete' | 'skip', reason: string) => any onStepOutcome: (step: string | undefined, outcome: string | 'end' | 'end-confirmed', reason: string) => any
}): string | string[] | boolean | any { }): string | string[] | boolean | any {
const functionName = functionCall.name; const functionName = functionCall.name;
try { try {
@ -531,9 +561,13 @@ You can use this function to report the outcome of each step as you work through
callbacks.onFileDelete(functionArgs.filePath!); callbacks.onFileDelete(functionArgs.filePath!);
break; break;
case 'reportStepOutcome': case 'reportStepOutcome':
console.debug(` - received reportStepOutcome function call: ${functionArgs.outcome} - ${functionArgs.reason}`); console.debug(` - received reportStepOutcome: ${functionArgs.step} - ${functionArgs.outcome} - ${functionArgs.description}`);
callbacks.onStepOutcome(functionArgs.outcome!, functionArgs.reason!); callbacks.onStepOutcome(functionArgs.step, functionArgs.outcome!, functionArgs.description!);
functionResponse = `Step outcome recorded: ${functionArgs.outcome} - ${functionArgs.reason}`; functionResponse = {
step: functionArgs.step,
outcome: functionArgs.outcome,
reason: functionArgs.description,
};
break; break;
default: default:
throw new Error(`Unknown function: ${functionName}`); throw new Error(`Unknown function: ${functionName}`);
@ -551,17 +585,26 @@ You can use this function to report the outcome of each step as you work through
rootPath: string, rootPath: string,
geminiResponse: GeminiResponse = { geminiResponse: GeminiResponse = {
stepOutcomes: [], stepOutcomes: [],
fileDeleted: [], filesDeleted: [],
fileWritten: [], filesWritten: [],
modelResponses: [] modelResponses: []
}): Promise<GeminiResponse> { }): Promise<GeminiResponse> {
// Generate content in a streaming fashion // Generate content in a streaming fashion
const streamGenerateContentResult = await generativeModel.generateContentStream(request); const streamGenerateContentResult = await generativeModel.generateContentStream(request);
const pendingFunctionCalls = []; const pendingFunctionCalls = [];
let endReceived = false;
// Process the streaming response // Process the streaming response
for await (const item of streamGenerateContentResult.stream) { for await (const item of streamGenerateContentResult.stream) {
const inputTokens = item.usageMetadata?.promptTokenCount ?? 0;
const outputTokens = item.usageMetadata?.candidatesTokenCount ?? 0;
const totalTokens = item.usageMetadata?.totalTokenCount ?? 0;
geminiResponse.inputCost = (geminiResponse.inputCost ?? 0) + inputTokens;
geminiResponse.outputCost = (geminiResponse.outputCost ?? 0) + outputTokens;
geminiResponse.totalCost = (geminiResponse.totalCost ?? 0) + totalTokens;
// Iterate over every part in the response // Iterate over every part in the response
let generateContentCandidates = item.candidates ?? []; let generateContentCandidates = item.candidates ?? [];
if (generateContentCandidates.length === 0) { if (generateContentCandidates.length === 0) {
@ -591,33 +634,59 @@ You can use this function to report the outcome of each step as you work through
} }
} }
// TODO: drop old content above 1M tokens
const updatedRequestContents = [
...request.contents,
];
// Process any function calls that were detected // Process any function calls that were detected
if (pendingFunctionCalls.length > 0) { if (pendingFunctionCalls.length > 0) {
// TODO: drop old content above 1M tokens
const updatedRequestContents = [
...request.contents,
];
for (const functionCall of pendingFunctionCalls) { for (const functionCall of pendingFunctionCalls) {
const responseData = this.processFunctionCall(functionCall, rootPath, { const responseData = this.processFunctionCall(functionCall, rootPath, {
onFileWritten: (f) => geminiResponse.fileWritten.push(f), onFileWritten: (f) => {
onFileDelete: (f) => geminiResponse.fileDeleted.push(f), if (!geminiResponse.filesWritten.includes(f)) {
onStepOutcome: (outcome, reason) => geminiResponse.stepOutcomes.push({ geminiResponse.filesWritten.push(f);
decision: outcome, }
reason: reason },
}) onFileDelete: (f) => {
if (!geminiResponse.filesDeleted.includes(f)) {
geminiResponse.filesDeleted.push(f)
}
},
onStepOutcome: (step, outcome, reason) => {
if (outcome === 'end') {
const updatedContent = this.createReevaluationContrent();
updatedRequestContents.push(...updatedContent);
} else if (outcome === 'end-confirmed') {
console.log('End confirmed');
endReceived = true;
} else {
geminiResponse.stepOutcomes.push({
step: step,
outcomes: outcome,
reason: reason
});
}
}
}); });
const contents = this.createFunctionExchangeContents(functionCall, responseData); const contents = this.createFunctionExchangeContents(functionCall, responseData);
updatedRequestContents.push(...contents); updatedRequestContents.push(...contents);
} }
// Submit a new request
const updatedRequest: GenerateContentRequest = {
contents: updatedRequestContents,
tools: this.fileOperationTools,
};
return this.handleGeminiStream(generativeModel, updatedRequest, rootPath, geminiResponse);
} else { } else {
console.debug("No function calls detected in response.")
const updatedContent = this.createReevaluationContrent();
updatedRequestContents.push(...updatedContent);
}
if (endReceived) {
return geminiResponse; return geminiResponse;
} }
// Submit a new request
const updatedRequest: GenerateContentRequest = {
contents: updatedRequestContents,
tools: this.fileOperationTools,
};
return this.handleGeminiStream(generativeModel, updatedRequest, rootPath, geminiResponse);
} }
} }

View File

@ -5,24 +5,25 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import {ProcessResult, RepoCredentials} from '../types'; import {ProcessResult, RepoCredentials} from '../types';
import { import {
RepositoryService as SharedRepositoryService, GeminiService,
Project,
PullRequestService as SharedPullRequestService, PullRequestService as SharedPullRequestService,
GeminiService, Project RepositoryService as SharedRepositoryService
} from 'shared-functions'; } from 'shared-functions';
import {ProjectService} from './project-service'; import {ProjectService} from './project-service';
import {ProjectTestSpecsService} from './project-test-specs-service'; import {ProjectTestSpecsService} from './project-test-specs-service';
import { import {
DRY_RUN_SKIP_COMMITS, DRY_RUN_SKIP_COMMITS,
DRY_RUN_SKIP_GEMINI,
GEMINI_MODEL,
getGiteaCredentials, getGiteaCredentials,
getGithubCredentials, getGithubCredentials,
getMainRepoCredentials, getMainRepoCredentials,
GOOGLE_CLOUD_LOCATION,
GOOGLE_CLOUD_PROJECT_ID,
MAIN_REPO_URL, MAIN_REPO_URL,
USE_LOCAL_REPO, USE_LOCAL_REPO,
validateConfig, validateConfig
GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION,
GEMINI_MODEL,
DRY_RUN_SKIP_GEMINI
} from '../config'; } from '../config';
export class ProcessorService { export class ProcessorService {
@ -231,8 +232,18 @@ export class ProcessorService {
await this.sharedRepositoryService.pushChanges(projectRepoPath, branchName, credentials); await this.sharedRepositoryService.pushChanges(projectRepoPath, branchName, credentials);
// Generate PR description using Gemini // Generate PR description using Gemini
const description = await this.geminiService.generatePullRequestDescription( const modelResponses = result.modelResponses ?? [];
"Test spec implementation", const lastModelResponse = modelResponses.slice(Math.max(modelResponses.length - 10, 0), modelResponses.length);
const changeDescription = `
feature spec implementation.
${result.totalCost} tokens consumed to write ${result.filesWritten?.length ?? 0} files`;
`last model responses:
${lastModelResponse.join('\n')}
`;
const prDescription = await this.geminiService.generatePullRequestDescription(
changeDescription,
result.gitPatch result.gitPatch
); );
@ -245,7 +256,7 @@ export class ProcessorService {
branchName, branchName,
credentials, credentials,
title, title,
description prDescription
); );
console.log(`Created pull request: ${pullRequestUrl}`); console.log(`Created pull request: ${pullRequestUrl}`);

View File

@ -41,7 +41,7 @@ export class ProjectTestSpecsService {
if ((result.filesWritten?.length ?? 0) > 0 || (result.filesRemoved?.length ?? 0) > 0) { if ((result.filesWritten?.length ?? 0) > 0 || (result.filesRemoved?.length ?? 0) > 0) {
try { try {
console.log(`Generating git patch for project ${project.name} with ${result.filesWritten} files written`); console.log(`Generating git patch for project ${project.name} with ${result.filesWritten?.length} files written`);
gitPatch = await this.sharedRepositoryService.generateGitPatch(projectRepoPath); gitPatch = await this.sharedRepositoryService.generateGitPatch(projectRepoPath);
} catch (error) { } catch (error) {
@ -87,12 +87,13 @@ export class ProjectTestSpecsService {
relevantFiles relevantFiles
); );
console.log(`ProjectTestSpecsService: Completed processing project (Files written: ${result.fileWritten.length})`); console.log(`ProjectTestSpecsService: Completed processing project (Files written: ${result.filesWritten.length})`);
return { return {
project: project, project: project,
success: true, success: true,
filesWritten: result.fileWritten, filesWritten: result.filesWritten,
filesRemoved: result.fileDeleted, filesRemoved: result.filesDeleted,
totalCost: result.totalCost
}; };
} catch (error) { } catch (error) {
console.error(`Error processing project ${project.name}:`, error); console.error(`Error processing project ${project.name}:`, error);
@ -155,8 +156,8 @@ export class ProjectTestSpecsService {
return { return {
modelResponses: [], modelResponses: [],
stepOutcomes: [], stepOutcomes: [],
fileDeleted: [], filesDeleted: [],
fileWritten: [] filesWritten: []
}; };
} }

View File

@ -36,6 +36,8 @@ export interface ProcessResult {
gitPatch?: string; gitPatch?: string;
filesWritten?: string[]; filesWritten?: string[];
filesRemoved?: string[]; filesRemoved?: string[];
totalCost?: number;
modelResponses?: string[];
} }
/** /**

View File

@ -1,4 +1,40 @@
This is your guideline for the implementation of the feature file: Implement tests according to the cucumber ".feature" files.
- Iterate over cucumber ".feature" definition files in the `nitro-it/src/test/resources/workitems/` directory. - Iterate over cucumber ".feature" definition files in the `nitro-it/src/test/resources/workitems/` directory.
- For each of them create all required files to implement the feature. - For each of them create all required files to implement the tests.
- Use quarkus apis and best practices
- All files and all their method must be correctly implemented, without any TODO or stub or placeholder.
- The code produced must be ready for test driven development without any adaptation required.
- The tests are business-driven integration tests: A real api must be accessed to ensure proper application
behavior.
- Scan the existing api in nitro-domain-api/src/main/java to implement http requests to the api endpoints.
- Use the following techniques to identify the relevant resources:
- search for patterns like 'class Ws*<resource-name-camel-case>*' to identify api models file names
- search for patterns like 'interface Ws*<resource-name-camel-case>*Controller' to identify api controller file
names
- Retrieve files content to inspect their structure and interactions
- Grep a class name to discover where its used across the codebase
- fetch the pom.xml files to inspect the dependencies and their versions
- Get a complete understanding of the relevant resources, how they relate to each other, and the available operations.
- Get a complete understanding of the various entities composing the business resources
- Create missing global configuration in nitro-it/src/test/resources/application-bdd.properties
- create or update @ApplicationScoped services in nitro-it/src/test/java/be/fiscalteam/nitro/bdd/services/
to implement the test logic
- Those services must be fully implemented and make actual http requests to the api endpoints when called.
For each feature file, create or update the implementation in nitro-it/src/test/java/be/fiscalteam/nitro/bdd/features/<
feature-name>/
- Create or update a "ScenarioState.java" service annotated @ScenarioScope.
This service contains the state for each scenario execution.
- Create or update a "FeatureSteps.java" class to implement the step definitions from the feature file.
This class injects the ScenarioState and other services. Add javadoc referencing the feature file.
Use Given/When/Then/And annotations from io.cucumber.java.en to implement each step in the feature file.
- Step definition implementations must be short, passing data between @ApplicationScoped services and the @ScenarioScope
state. Implement or reuse services in nitro-it/src/test/java/be/fiscalteam/nitro/bdd/services/ if needed.
- No hardcoded values should be present - use constant files or obtain data from services.
- Supporting data and constants can be defined in resource files in the
nitro-it/src/test/resources/be/fiscalteam/nitro/bdd/features/<feature-name>/ directory when required