genaicode 0.0.33 → 0.0.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genaicode",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
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',
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert';
2
- import { functionDefs } from './function-calling.js';
2
+ import { functionDefs } from '../prompt/function-calling.js';
3
3
 
4
4
  /**
5
5
  * Common function to print token usage and estimated cost
@@ -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 './function-calling.js';
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',
@@ -209,7 +209,7 @@ describe('runCodegen', () => {
209
209
  {
210
210
  name: 'codegenSummary',
211
211
  args: {
212
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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. It contains a list of file paths that will be subject to code generation request, and also a list of file paths that make sense to use as a context for code generation requests.',
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
- files: {
55
+ contextPaths: {
51
56
  type: 'array',
52
- description: 'An array of proposed file updates.',
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: 'Proposed update of a file, the path, and the method of update',
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 LLm request',
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', 'temperature', 'prompt', 'contextImageAssets', 'cheap'],
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: ['files', 'contextPaths', 'explanation'],
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: 'The file path to create.',
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: 'The content for the new file.',
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 '../ai-service/function-calling.js';
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 baseResult = await generateContentFn(prompt, functionDefs, 'codegenSummary', temperature, cheap);
47
+ let baseRequest = [prompt, functionDefs, 'codegenSummary', temperature, cheap];
48
+ let baseResult = await generateContentFn(...baseRequest);
47
49
 
48
- const codegenSummaryRequest = baseResult.find((call) => call.name === 'codegenSummary');
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.files), 'files is not an array');
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.files.map((file) => file.path),
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.files) {
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 partialResult = await generateContentFn(
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
- partialResult = await generateContentFn(
166
- prompt,
167
- functionDefs,
168
- 'updateFile',
169
- file.temperature,
170
- file.cheap === true,
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
- files: [{ path: 'test.js', updateToolName: 'patchFile' }],
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
- files: [
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
- files: [{ path: 'test.js', updateToolName: 'updateFile' }],
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
  });