genaicode 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +3 -2
- package/src/ai-service/anthropic.js +1 -0
- package/src/ai-service/common.js +1 -1
- package/src/ai-service/common.test.js +2 -1
- package/src/ai-service/vertex-ai-claude.js +1 -0
- package/src/main/codegen.test.js +3 -3
- package/src/prompt/function-calling-validate.js +25 -0
- package/src/{ai-service → prompt}/function-calling.js +26 -19
- package/src/prompt/prompt-service.js +82 -15
- package/src/prompt/prompt-service.test.js +107 -6
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="media/logo-dark.png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="media/logo.png">
|
|
5
|
+
<img alt="GenAIcode Logo." src="media/logo.png" width="100%" height="auto">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
10
|
+
|
|
1
11
|
# Programming on steroids
|
|
2
12
|
|
|
3
13
|
<a href="https://www.npmjs.com/package/genaicode">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genaicode",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"author": "Grzegorz Tańczyk",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@anthropic-ai/sdk": "^0.24.3",
|
|
47
47
|
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
|
48
|
-
"@google-cloud/vertexai": "^1.3.0",
|
|
49
48
|
"@google-cloud/aiplatform": "^3.25.0",
|
|
49
|
+
"@google-cloud/vertexai": "^1.3.0",
|
|
50
50
|
"@imgly/background-removal-node": "^1.4.5",
|
|
51
51
|
"diff": "^5.2.0",
|
|
52
52
|
"image-size": "^1.1.1",
|
|
53
|
+
"jsonschema": "^1.4.1",
|
|
53
54
|
"mime-types": "^2.1.35",
|
|
54
55
|
"openai": "^4.52.7",
|
|
55
56
|
"please-upgrade-node": "^3.2.0",
|
|
@@ -22,6 +22,7 @@ export async function generateContent(prompt, functionDefs, requiredFunctionName
|
|
|
22
22
|
tool_use_id: response.call_id ?? response.name,
|
|
23
23
|
content: response.content,
|
|
24
24
|
type: 'tool_result',
|
|
25
|
+
is_error: response.isError === true,
|
|
25
26
|
})),
|
|
26
27
|
...(item.images ?? []).map((image) => ({
|
|
27
28
|
type: 'image',
|
package/src/ai-service/common.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { printTokenUsageAndCost, processFunctionCalls } from './common.js';
|
|
3
|
-
import { functionDefs } from '
|
|
3
|
+
import { functionDefs } from '../prompt/function-calling.js';
|
|
4
4
|
|
|
5
5
|
// Mock cli-params.js
|
|
6
6
|
vi.mock('../cli/cli-params.js', () => ({
|
|
@@ -12,6 +12,7 @@ vi.mock('../cli/cli-params.js', () => ({
|
|
|
12
12
|
cheap: false,
|
|
13
13
|
geminiBlockNone: false,
|
|
14
14
|
requireExplanations: false,
|
|
15
|
+
vision: false,
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
18
|
describe('printTokenUsageAndCost', () => {
|
|
@@ -28,6 +28,7 @@ export async function generateContent(prompt, functionDefs, requiredFunctionName
|
|
|
28
28
|
type: 'tool_result',
|
|
29
29
|
tool_use_id: response.call_id ?? response.name,
|
|
30
30
|
content: response.content,
|
|
31
|
+
is_error: response.isError === true,
|
|
31
32
|
})),
|
|
32
33
|
...(item.images ?? []).map((image) => ({
|
|
33
34
|
type: 'image',
|
package/src/main/codegen.test.js
CHANGED
|
@@ -209,7 +209,7 @@ describe('runCodegen', () => {
|
|
|
209
209
|
{
|
|
210
210
|
name: 'codegenSummary',
|
|
211
211
|
args: {
|
|
212
|
-
|
|
212
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
213
213
|
contextPaths: [],
|
|
214
214
|
explanation: 'Mock summary with image generation failure',
|
|
215
215
|
},
|
|
@@ -237,7 +237,7 @@ describe('runCodegen', () => {
|
|
|
237
237
|
{
|
|
238
238
|
name: 'codegenSummary',
|
|
239
239
|
args: {
|
|
240
|
-
|
|
240
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
241
241
|
contextPaths: [],
|
|
242
242
|
explanation: 'Mock summary with image generation failure',
|
|
243
243
|
},
|
|
@@ -293,7 +293,7 @@ describe('runCodegen', () => {
|
|
|
293
293
|
{
|
|
294
294
|
name: 'codegenSummary',
|
|
295
295
|
args: {
|
|
296
|
-
|
|
296
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
297
297
|
contextPaths: [],
|
|
298
298
|
explanation: 'Mock summary with cheap image generation',
|
|
299
299
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Validator } from 'jsonschema';
|
|
2
|
+
import { functionDefs } from './function-calling.js';
|
|
3
|
+
|
|
4
|
+
export function validateFunctionCall(call) {
|
|
5
|
+
const validator = new Validator();
|
|
6
|
+
const functionDef = functionDefs.find((def) => def.name === call.name);
|
|
7
|
+
|
|
8
|
+
if (!functionDef) {
|
|
9
|
+
return {
|
|
10
|
+
errors: [
|
|
11
|
+
{
|
|
12
|
+
message: `Function "${call.name}" is not defined.`,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const validationResult = validator.validate(call.args, functionDef.parameters);
|
|
19
|
+
|
|
20
|
+
if (validationResult.errors.length > 0) {
|
|
21
|
+
return validationResult;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requireExplanations, temperature, cheap } from '../cli/cli-params.js';
|
|
1
|
+
import { requireExplanations, temperature, cheap, vision } from '../cli/cli-params.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Function definitions for function calling feature
|
|
@@ -43,24 +43,38 @@ export const functionDefs = [
|
|
|
43
43
|
{
|
|
44
44
|
name: 'codegenSummary',
|
|
45
45
|
description:
|
|
46
|
-
'This function is called with a summary of proposed updates.
|
|
46
|
+
'This function is called with a summary of proposed updates.' +
|
|
47
|
+
'Summary is a data structure(object) which contains:\n' +
|
|
48
|
+
'- `fileUpdates`: list of proposed file updates that will be subject to subsequent code generation request\n' +
|
|
49
|
+
'- `contextPaths`: list of file paths that make sense to use as a context for code generation requests.' +
|
|
50
|
+
'- `explanation`: general explanation of planned code generation updates\n' +
|
|
51
|
+
'It is crticially important to adhere to the schema of parameters',
|
|
47
52
|
parameters: {
|
|
48
53
|
type: 'object',
|
|
49
54
|
properties: {
|
|
50
|
-
|
|
55
|
+
contextPaths: {
|
|
51
56
|
type: 'array',
|
|
52
|
-
description:
|
|
57
|
+
description:
|
|
58
|
+
'An array of absolute paths of files that should be used to provide context for the following updates. Context files could be for example the dependencies, or files that depend on one of the files that we want to update in the next step.',
|
|
59
|
+
items: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
fileUpdates: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
description: 'An array of proposed file updates, each update is an object with several properties.',
|
|
53
66
|
items: {
|
|
54
67
|
type: 'object',
|
|
55
|
-
description:
|
|
68
|
+
description:
|
|
69
|
+
'Proposed update of a file. The update is an object which contains properties like file path, update tool name, and few other important properties.',
|
|
56
70
|
properties: {
|
|
57
71
|
path: { type: 'string', description: 'An absolute path of the project file that will be updated' },
|
|
58
72
|
updateToolName: {
|
|
59
73
|
type: 'string',
|
|
60
74
|
enum: [
|
|
75
|
+
'createFile',
|
|
61
76
|
'updateFile',
|
|
62
77
|
'patchFile',
|
|
63
|
-
'createFile',
|
|
64
78
|
'deleteFile',
|
|
65
79
|
'createDirectory',
|
|
66
80
|
'moveFile',
|
|
@@ -92,19 +106,12 @@ export const functionDefs = [
|
|
|
92
106
|
contextImageAssets: {
|
|
93
107
|
type: 'array',
|
|
94
108
|
description:
|
|
95
|
-
'A list of of absolute image asset paths that should be included to the context of
|
|
109
|
+
'A list of of absolute image asset paths that should be included to the context of LLM request.' +
|
|
110
|
+
'This parameter must be used if there is a need to analyze an image.',
|
|
96
111
|
items: { type: 'string' },
|
|
97
112
|
},
|
|
98
113
|
},
|
|
99
|
-
required: ['path', 'updateToolName',
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
contextPaths: {
|
|
103
|
-
type: 'array',
|
|
104
|
-
description:
|
|
105
|
-
'An array of absolute paths of files that should be used to provide context for the following updates. Context files could be for example the dependencies, or files that depend on one of the files that we want to update in the next step.',
|
|
106
|
-
items: {
|
|
107
|
-
type: 'string',
|
|
114
|
+
required: ['path', 'updateToolName', ...(vision ? ['contextImageAssets'] : [])],
|
|
108
115
|
},
|
|
109
116
|
},
|
|
110
117
|
explanation: {
|
|
@@ -112,7 +119,7 @@ export const functionDefs = [
|
|
|
112
119
|
description: 'Explanation of planned changes or explanation of reasoning for no code changes',
|
|
113
120
|
},
|
|
114
121
|
},
|
|
115
|
-
required: ['
|
|
122
|
+
required: ['fileUpdates', 'contextPaths'],
|
|
116
123
|
},
|
|
117
124
|
},
|
|
118
125
|
{
|
|
@@ -183,11 +190,11 @@ Index: filename.js
|
|
|
183
190
|
properties: {
|
|
184
191
|
filePath: {
|
|
185
192
|
type: 'string',
|
|
186
|
-
description: '
|
|
193
|
+
description: 'Path of the file that will be created, it must not be empty.',
|
|
187
194
|
},
|
|
188
195
|
newContent: {
|
|
189
196
|
type: 'string',
|
|
190
|
-
description: '
|
|
197
|
+
description: 'Content of the file that will be created, it must no be empty.',
|
|
191
198
|
},
|
|
192
199
|
explanation: {
|
|
193
200
|
type: 'string',
|
|
@@ -5,9 +5,10 @@ import mime from 'mime-types';
|
|
|
5
5
|
|
|
6
6
|
import { getSystemPrompt } from './systemprompt.js';
|
|
7
7
|
import { getCodeGenPrompt } from './prompt-codegen.js';
|
|
8
|
-
import { functionDefs } from '
|
|
8
|
+
import { functionDefs } from './function-calling.js';
|
|
9
9
|
import { getSourceCode, getImageAssets } from '../files/read-files.js';
|
|
10
10
|
import { disableContextOptimization, temperature, vision, cheap } from '../cli/cli-params.js';
|
|
11
|
+
import { validateFunctionCall } from './function-calling-validate.js';
|
|
11
12
|
|
|
12
13
|
/** A function that communicates with model using */
|
|
13
14
|
export async function promptService(generateContentFn, generateImageFn, codegenPrompt = getCodeGenPrompt()) {
|
|
@@ -43,16 +44,20 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
43
44
|
|
|
44
45
|
prompt.slice(-1)[0].text = messages.prompt;
|
|
45
46
|
|
|
46
|
-
let
|
|
47
|
+
let baseRequest = [prompt, functionDefs, 'codegenSummary', temperature, cheap];
|
|
48
|
+
let baseResult = await generateContentFn(...baseRequest);
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
let codegenSummaryRequest = baseResult.find((call) => call.name === 'codegenSummary');
|
|
49
51
|
|
|
50
52
|
if (codegenSummaryRequest) {
|
|
51
53
|
// Second stage: for each file request the actual code updates
|
|
52
54
|
console.log('Received codegen summary, will collect partial updates', codegenSummaryRequest.args);
|
|
53
55
|
|
|
56
|
+
baseResult = await validateAndRecoverSingleResult(baseRequest, baseResult, messages, generateContentFn);
|
|
57
|
+
codegenSummaryRequest = baseResult.find((call) => call.name === 'codegenSummary');
|
|
58
|
+
|
|
54
59
|
// Sometimes the result happens to be a string
|
|
55
|
-
assert(Array.isArray(codegenSummaryRequest.args.
|
|
60
|
+
assert(Array.isArray(codegenSummaryRequest.args.fileUpdates), 'fileUpdates is not an array');
|
|
56
61
|
assert(Array.isArray(codegenSummaryRequest.args.contextPaths), 'contextPaths is not an array');
|
|
57
62
|
|
|
58
63
|
if (codegenSummaryRequest.args.contextPaths.length > 0 && !disableContextOptimization) {
|
|
@@ -60,7 +65,7 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
60
65
|
// Monkey patch the initial getSourceCode, do not send parts of source code that are consider irrelevant
|
|
61
66
|
getSourceCodeRequest.args = {
|
|
62
67
|
filePaths: [
|
|
63
|
-
...codegenSummaryRequest.args.
|
|
68
|
+
...codegenSummaryRequest.args.fileUpdates.map((file) => file.path),
|
|
64
69
|
...codegenSummaryRequest.args.contextPaths,
|
|
65
70
|
],
|
|
66
71
|
};
|
|
@@ -77,7 +82,7 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
77
82
|
|
|
78
83
|
const result = [];
|
|
79
84
|
|
|
80
|
-
for (const file of codegenSummaryRequest.args.
|
|
85
|
+
for (const file of codegenSummaryRequest.args.fileUpdates) {
|
|
81
86
|
console.log('Collecting partial update for: ' + file.path + ' using tool: ' + file.updateToolName);
|
|
82
87
|
console.log('- Prompt:', file.prompt);
|
|
83
88
|
console.log('- Temperature', file.temperature);
|
|
@@ -93,7 +98,7 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
93
98
|
prompt.push({ type: 'user', text: file.prompt ?? messages.partialPromptTemplate(file.path) });
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
if (vision) {
|
|
101
|
+
if (vision && file.contextImageAssets) {
|
|
97
102
|
prompt.slice(-1)[0].images = file.contextImageAssets.map((path) => ({
|
|
98
103
|
path,
|
|
99
104
|
base64url: fs.readFileSync(path, 'base64'),
|
|
@@ -101,13 +106,14 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
101
106
|
}));
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
let
|
|
109
|
+
let partialRequest = [
|
|
105
110
|
prompt,
|
|
106
111
|
functionDefs,
|
|
107
112
|
file.updateToolName,
|
|
108
113
|
file.temperature ?? temperature,
|
|
109
114
|
file.cheap === true,
|
|
110
|
-
|
|
115
|
+
];
|
|
116
|
+
let partialResult = await generateContentFn(...partialRequest);
|
|
111
117
|
|
|
112
118
|
let getSourceCodeCall = partialResult.find((call) => call.name === 'getSourceCode');
|
|
113
119
|
assert(!getSourceCodeCall, 'Unexpected getSourceCode: ' + JSON.stringify(getSourceCodeCall));
|
|
@@ -141,6 +147,13 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
141
147
|
},
|
|
142
148
|
});
|
|
143
149
|
}
|
|
150
|
+
} else {
|
|
151
|
+
partialResult = await validateAndRecoverSingleResult(
|
|
152
|
+
partialRequest,
|
|
153
|
+
partialResult,
|
|
154
|
+
messages,
|
|
155
|
+
generateContentFn,
|
|
156
|
+
);
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
// Verify if patchFile is one of the functions called, and test if patch is valid and can be applied successfully
|
|
@@ -162,12 +175,14 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
162
175
|
console.log(`Patch could not be applied for ${filePath}. Retrying without patchFile function.`);
|
|
163
176
|
|
|
164
177
|
// Rerun content generation without patchFile function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
partialRequest = [prompt, functionDefs, 'updateFile', file.temperature ?? temperature, file.cheap === true];
|
|
179
|
+
partialResult = await generateContentFn(...partialRequest);
|
|
180
|
+
|
|
181
|
+
partialResult = await validateAndRecoverSingleResult(
|
|
182
|
+
partialRequest,
|
|
183
|
+
partialResult,
|
|
184
|
+
messages,
|
|
185
|
+
generateContentFn,
|
|
171
186
|
);
|
|
172
187
|
|
|
173
188
|
let getSourceCodeCall = partialResult.find((call) => call.name === 'getSourceCode');
|
|
@@ -198,6 +213,56 @@ export async function promptService(generateContentFn, generateImageFn, codegenP
|
|
|
198
213
|
}
|
|
199
214
|
}
|
|
200
215
|
|
|
216
|
+
async function validateAndRecoverSingleResult(
|
|
217
|
+
[prompt, functionDefs, requiredFunctionName, temperature, cheap],
|
|
218
|
+
result,
|
|
219
|
+
messages,
|
|
220
|
+
generateContentFn,
|
|
221
|
+
) {
|
|
222
|
+
if (result.length !== 1) {
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const call = result[0];
|
|
227
|
+
const validatorError = validateFunctionCall(call);
|
|
228
|
+
if (validatorError) {
|
|
229
|
+
console.log('Invalid function call', call, validatorError);
|
|
230
|
+
|
|
231
|
+
prompt.push(
|
|
232
|
+
{ type: 'assistant', functionCalls: [call] },
|
|
233
|
+
{
|
|
234
|
+
type: 'user',
|
|
235
|
+
text: messages.invalidFunctionCall,
|
|
236
|
+
functionResponses: [
|
|
237
|
+
{
|
|
238
|
+
name: call.name,
|
|
239
|
+
call_id: call.id,
|
|
240
|
+
content: JSON.stringify({ args: call.args, error: validatorError }),
|
|
241
|
+
isError: true,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
console.log('Trying to recover...');
|
|
248
|
+
if (cheap) {
|
|
249
|
+
console.log('Disabling --cheap for recovery.');
|
|
250
|
+
}
|
|
251
|
+
result = await generateContentFn(prompt, functionDefs, requiredFunctionName, temperature, false);
|
|
252
|
+
console.log('Recover result:', result);
|
|
253
|
+
|
|
254
|
+
if (result.length === 1) {
|
|
255
|
+
const recoveryError = validateFunctionCall(result[0]);
|
|
256
|
+
assert(!recoveryError, 'Recovery failed');
|
|
257
|
+
console.log('Recovery was sucessful');
|
|
258
|
+
} else {
|
|
259
|
+
console.log('Unexpected number of function calls', result);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
201
266
|
/**
|
|
202
267
|
* Function to prepare messages for AI services
|
|
203
268
|
*/
|
|
@@ -216,5 +281,7 @@ function prepareMessages(prompt) {
|
|
|
216
281
|
partialPromptTemplate(path) {
|
|
217
282
|
return `Thank you for providing the summary, now suggest changes for the \`${path}\` file using appropriate tools.`;
|
|
218
283
|
},
|
|
284
|
+
invalidFunctionCall:
|
|
285
|
+
'Function call was invalid, please analyze the error and respond with corrected function call.',
|
|
219
286
|
};
|
|
220
287
|
}
|
|
@@ -124,7 +124,7 @@ describe('promptService', () => {
|
|
|
124
124
|
{
|
|
125
125
|
name: 'codegenSummary',
|
|
126
126
|
args: {
|
|
127
|
-
|
|
127
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'patchFile' }],
|
|
128
128
|
contextPaths: [],
|
|
129
129
|
explanation: 'Mock summary',
|
|
130
130
|
},
|
|
@@ -213,7 +213,7 @@ describe('promptService', () => {
|
|
|
213
213
|
{
|
|
214
214
|
name: 'codegenSummary',
|
|
215
215
|
args: {
|
|
216
|
-
|
|
216
|
+
fileUpdates: [
|
|
217
217
|
{
|
|
218
218
|
path: 'test.js',
|
|
219
219
|
updateToolName: 'updateFile',
|
|
@@ -296,7 +296,7 @@ describe('promptService', () => {
|
|
|
296
296
|
{
|
|
297
297
|
name: 'codegenSummary',
|
|
298
298
|
args: {
|
|
299
|
-
|
|
299
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
300
300
|
contextPaths: ['context1.js', 'context2.js'],
|
|
301
301
|
explanation: 'Mock summary with context',
|
|
302
302
|
},
|
|
@@ -341,7 +341,7 @@ describe('promptService', () => {
|
|
|
341
341
|
{
|
|
342
342
|
name: 'codegenSummary',
|
|
343
343
|
args: {
|
|
344
|
-
|
|
344
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
345
345
|
contextPaths: ['context1.js', 'context2.js'],
|
|
346
346
|
explanation: 'Mock summary without context optimization',
|
|
347
347
|
},
|
|
@@ -386,7 +386,7 @@ describe('promptService', () => {
|
|
|
386
386
|
{
|
|
387
387
|
name: 'codegenSummary',
|
|
388
388
|
args: {
|
|
389
|
-
|
|
389
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
390
390
|
contextPaths: [],
|
|
391
391
|
explanation: 'Mock summary with image generation',
|
|
392
392
|
},
|
|
@@ -436,7 +436,7 @@ describe('promptService', () => {
|
|
|
436
436
|
{
|
|
437
437
|
name: 'codegenSummary',
|
|
438
438
|
args: {
|
|
439
|
-
|
|
439
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'updateFile' }],
|
|
440
440
|
contextPaths: [],
|
|
441
441
|
explanation: 'Mock summary with image generation failure',
|
|
442
442
|
},
|
|
@@ -497,4 +497,105 @@ describe('promptService', () => {
|
|
|
497
497
|
expect(vertexAi.generateContent).toHaveBeenCalledTimes(1);
|
|
498
498
|
expect(result).toEqual(mockUnexpectedResponse);
|
|
499
499
|
});
|
|
500
|
+
|
|
501
|
+
describe('validateAndRecoverSingleResult', () => {
|
|
502
|
+
it('should successfully recover from an invalid function call', async () => {
|
|
503
|
+
cliParams.vertexAi = true;
|
|
504
|
+
const mockInvalidCall = [
|
|
505
|
+
{
|
|
506
|
+
name: 'codegenSummary',
|
|
507
|
+
args: {
|
|
508
|
+
files: [{ path: 'test.js', updateToolName: 'patchFile' }],
|
|
509
|
+
contextPaths: [],
|
|
510
|
+
explanation: 'Mock summary',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
const mockValidCall = [
|
|
515
|
+
{
|
|
516
|
+
name: 'codegenSummary',
|
|
517
|
+
args: {
|
|
518
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'patchFile' }],
|
|
519
|
+
contextPaths: [],
|
|
520
|
+
explanation: 'Mock summary',
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
vertexAi.generateContent
|
|
526
|
+
.mockResolvedValueOnce(mockInvalidCall)
|
|
527
|
+
.mockResolvedValueOnce(mockValidCall)
|
|
528
|
+
.mockResolvedValueOnce([
|
|
529
|
+
{
|
|
530
|
+
name: 'updateFile',
|
|
531
|
+
args: {
|
|
532
|
+
filePath: 'test.js',
|
|
533
|
+
newContent: 'console.log("Unexpected response");',
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
]);
|
|
537
|
+
|
|
538
|
+
const result = await promptService(vertexAi.generateContent);
|
|
539
|
+
|
|
540
|
+
expect(vertexAi.generateContent).toHaveBeenCalledTimes(3);
|
|
541
|
+
expect(result).toEqual([
|
|
542
|
+
{
|
|
543
|
+
args: {
|
|
544
|
+
filePath: 'test.js',
|
|
545
|
+
newContent: 'console.log("Unexpected response");',
|
|
546
|
+
},
|
|
547
|
+
name: 'updateFile',
|
|
548
|
+
},
|
|
549
|
+
]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should handle unsuccessful recovery', async () => {
|
|
553
|
+
cliParams.vertexAi = true;
|
|
554
|
+
const mockInvalidCall = [
|
|
555
|
+
{
|
|
556
|
+
name: 'updateFile',
|
|
557
|
+
args: { filePath: 'test.js', invalidArg: 'This should not be here' },
|
|
558
|
+
},
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
vertexAi.generateContent
|
|
562
|
+
.mockResolvedValueOnce([
|
|
563
|
+
{
|
|
564
|
+
name: 'codegenSummary',
|
|
565
|
+
args: {
|
|
566
|
+
fileUpdates: [{ path: 'test.js', updateToolName: 'patchFile' }],
|
|
567
|
+
contextPaths: [],
|
|
568
|
+
explanation: 'Mock summary',
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
])
|
|
572
|
+
.mockResolvedValueOnce(mockInvalidCall)
|
|
573
|
+
.mockResolvedValueOnce(mockInvalidCall); // Second call also returns invalid result
|
|
574
|
+
|
|
575
|
+
await expect(promptService(vertexAi.generateContent)).rejects.toThrow('Recovery failed');
|
|
576
|
+
|
|
577
|
+
expect(vertexAi.generateContent).toHaveBeenCalledTimes(3);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should not attempt recovery for multiple valid function calls', async () => {
|
|
581
|
+
cliParams.vertexAi = true;
|
|
582
|
+
const mockValidCalls = [
|
|
583
|
+
{
|
|
584
|
+
name: 'updateFile',
|
|
585
|
+
args: { filePath: 'test1.js', newContent: 'console.log("File 1");' },
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'updateFile',
|
|
589
|
+
args: { filePath: 'test2.js', newContent: 'console.log("File 2");' },
|
|
590
|
+
},
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
vertexAi.generateContent.mockResolvedValueOnce(mockValidCalls);
|
|
594
|
+
|
|
595
|
+
const result = await promptService(vertexAi.generateContent);
|
|
596
|
+
|
|
597
|
+
expect(vertexAi.generateContent).toHaveBeenCalledTimes(1);
|
|
598
|
+
expect(result).toEqual(mockValidCalls);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
500
601
|
});
|