mdi-llmkit 0.1.0 → 1.0.1
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 +116 -34
- package/dist/src/comparison/compareLists.d.ts +97 -0
- package/dist/src/comparison/compareLists.js +375 -0
- package/dist/src/comparison/index.d.ts +1 -0
- package/dist/src/comparison/index.js +1 -0
- package/dist/src/gptApi/functions.d.ts +21 -0
- package/dist/src/gptApi/functions.js +154 -0
- package/dist/src/gptApi/gptConversation.d.ts +43 -0
- package/dist/src/gptApi/gptConversation.js +146 -0
- package/dist/src/gptApi/index.d.ts +3 -0
- package/dist/src/gptApi/index.js +3 -0
- package/dist/src/gptApi/jsonSchemaFormat.d.ts +14 -0
- package/dist/src/gptApi/jsonSchemaFormat.js +198 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/jsonSurgery/jsonSurgery.d.ts +81 -0
- package/dist/src/jsonSurgery/jsonSurgery.js +776 -0
- package/dist/src/jsonSurgery/placemarkedJSON.d.ts +57 -0
- package/dist/src/jsonSurgery/placemarkedJSON.js +151 -0
- package/dist/tests/comparison/compareLists.test.d.ts +1 -0
- package/dist/tests/comparison/compareLists.test.js +434 -0
- package/dist/tests/gptApi/gptConversation.test.d.ts +1 -0
- package/dist/tests/gptApi/gptConversation.test.js +157 -0
- package/dist/tests/gptApi/gptSubmit.test.d.ts +1 -0
- package/dist/tests/gptApi/gptSubmit.test.js +161 -0
- package/dist/tests/gptApi/jsonSchemaFormat.test.d.ts +1 -0
- package/dist/tests/gptApi/jsonSchemaFormat.test.js +372 -0
- package/dist/tests/jsonSurgery/jsonSurgery.test.d.ts +1 -0
- package/dist/tests/jsonSurgery/jsonSurgery.test.js +729 -0
- package/dist/tests/jsonSurgery/placemarkedJSON.test.d.ts +1 -0
- package/dist/tests/jsonSurgery/placemarkedJSON.test.js +209 -0
- package/dist/tests/setupEnv.d.ts +1 -0
- package/dist/tests/setupEnv.js +4 -0
- package/dist/tests/subpathExports.test.d.ts +1 -0
- package/dist/tests/subpathExports.test.js +47 -0
- package/package.json +18 -5
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { navigateToJSONPath, placemarkedJSONStringify, } from './placemarkedJSON.js';
|
|
2
|
+
import { GptConversation } from '../gptApi/gptConversation.js';
|
|
3
|
+
const JSON_SCHEMA_ANYOF_PRIMITIVE_OR_EMPTY = [
|
|
4
|
+
{
|
|
5
|
+
type: 'object',
|
|
6
|
+
properties: {
|
|
7
|
+
string_value: { type: 'string' },
|
|
8
|
+
},
|
|
9
|
+
required: ['string_value'],
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
numerical_value: { type: 'number' },
|
|
16
|
+
},
|
|
17
|
+
required: ['numerical_value'],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
boolean_value: { type: 'boolean' },
|
|
24
|
+
},
|
|
25
|
+
required: ['boolean_value'],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
null_value: { type: 'null' },
|
|
32
|
+
},
|
|
33
|
+
required: ['null_value'],
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
empty_object: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {},
|
|
42
|
+
required: [],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['empty_object'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
empty_array: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
items: { type: 'null' },
|
|
55
|
+
maxItems: 0,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ['empty_array'],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const JSON_SCHEMA_SET_VALUE = {
|
|
63
|
+
anyOf: [
|
|
64
|
+
...JSON_SCHEMA_ANYOF_PRIMITIVE_OR_EMPTY,
|
|
65
|
+
{
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
populated_array: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
items: { anyOf: JSON_SCHEMA_ANYOF_PRIMITIVE_OR_EMPTY },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ['populated_array'],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
populated_object: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
key_value_pairs: {
|
|
83
|
+
type: 'array',
|
|
84
|
+
items: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
key: { type: 'string' },
|
|
88
|
+
value: { anyOf: JSON_SCHEMA_ANYOF_PRIMITIVE_OR_EMPTY },
|
|
89
|
+
},
|
|
90
|
+
required: ['key', 'value'],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ['key_value_pairs'],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ['populated_object'],
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
const JSON_SCHEMA_JSON_PATH = {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: {
|
|
107
|
+
anyOf: [{ type: 'string' }, { type: 'number' }],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const unpackValueFromSetValueSchema = (value) => {
|
|
111
|
+
// Now we gotta unpack the value field.
|
|
112
|
+
if ('string_value' in value) {
|
|
113
|
+
return value.string_value;
|
|
114
|
+
}
|
|
115
|
+
else if ('numerical_value' in value) {
|
|
116
|
+
return value.numerical_value;
|
|
117
|
+
}
|
|
118
|
+
else if ('boolean_value' in value) {
|
|
119
|
+
return value.boolean_value;
|
|
120
|
+
}
|
|
121
|
+
else if ('null_value' in value) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
else if ('empty_object' in value) {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
else if ('empty_array' in value) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
else if ('populated_array' in value) {
|
|
131
|
+
// Recursively unpack the elements of the array as well,
|
|
132
|
+
// since they will also be wrapped in the same schema.
|
|
133
|
+
return value.populated_array.map(unpackValueFromSetValueSchema);
|
|
134
|
+
}
|
|
135
|
+
else if ('populated_object' in value) {
|
|
136
|
+
const obj = {};
|
|
137
|
+
value.populated_object.key_value_pairs.forEach((pair) => {
|
|
138
|
+
obj[pair.key] = unpackValueFromSetValueSchema(pair.value);
|
|
139
|
+
});
|
|
140
|
+
return obj;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw new Error(`Invalid value object: ${JSON.stringify(value)}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Error type reserved for failures from {@link jsonSurgery}.
|
|
148
|
+
* `obj` contains the object being modified, captured in whatever state it was
|
|
149
|
+
* left in at the moment the exception was thrown.
|
|
150
|
+
*/
|
|
151
|
+
export class JSONSurgeryError extends Error {
|
|
152
|
+
obj;
|
|
153
|
+
constructor(message, obj, options) {
|
|
154
|
+
super(message, options);
|
|
155
|
+
this.name = 'JSONSurgeryError';
|
|
156
|
+
this.obj = JSON.parse(JSON.stringify(obj));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Modifies a JSON object based on modification instructions using OpenAI's API.
|
|
161
|
+
* Does NOT modify the original object in place; instead, works with a copy and returns
|
|
162
|
+
* the modified copy.
|
|
163
|
+
* @param openaiClient The OpenAI client to use for modifications
|
|
164
|
+
* @param obj The JSON object to modify
|
|
165
|
+
* @param modificationInstructions Instructions describing the modifications to apply
|
|
166
|
+
* @param options Optional configuration object. See {@link JSONSurgeryOptions}.
|
|
167
|
+
* @returns A copy of the original object, modified according to the instructions.
|
|
168
|
+
*/
|
|
169
|
+
export const jsonSurgery = async (openaiClient, obj, modificationInstructions, options) => {
|
|
170
|
+
options = options || {};
|
|
171
|
+
if (obj === null || obj === undefined) {
|
|
172
|
+
throw new TypeError('The provided object is null or undefined', obj);
|
|
173
|
+
}
|
|
174
|
+
// DO NOT modify the originalObject in place.
|
|
175
|
+
obj = JSON.parse(JSON.stringify(obj));
|
|
176
|
+
const timeStarted = Date.now();
|
|
177
|
+
const convoBase = new GptConversation([], { openaiClient });
|
|
178
|
+
convoBase.addDeveloperMessage(`
|
|
179
|
+
You are an expert software developer AI assistant.
|
|
180
|
+
The user will show you a JSON object and provide modification instructions.
|
|
181
|
+
The modification instructions might not be entirely straightforward, so the
|
|
182
|
+
process of implementing these changes may require careful thought and planning.
|
|
183
|
+
Through a series of individual insertions, deletions, or updates, you will modify the JSON object
|
|
184
|
+
to satisfy the user's instructions.
|
|
185
|
+
You will not be doing this alone. I will be holding your hand through the entire process,
|
|
186
|
+
providing feedback and guidance after each modification. You will also be getting verification
|
|
187
|
+
from the system itself, to ensure that your modifications are valid and correct.
|
|
188
|
+
`);
|
|
189
|
+
convoBase.addUserMessage(`
|
|
190
|
+
|
|
191
|
+
Here is the JSON object to modify, in its original state prior to any modifications.
|
|
192
|
+
It has been formatted with placemarks (comments) to indicate the positions of elements for
|
|
193
|
+
better readability and easier navigation.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
${placemarkedJSONStringify(obj, 2, options.skippedKeys)}
|
|
198
|
+
`);
|
|
199
|
+
if (options.schemaDescription) {
|
|
200
|
+
convoBase.addUserMessage(`
|
|
201
|
+
Here is the schema definition for the JSON object, so that you know the expected structure
|
|
202
|
+
and data types of its properties. Make sure that your final results conform to this schema.
|
|
203
|
+
DO NOT introduce any properties or values that violate this schema!
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
${options.schemaDescription}
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
convoBase.addUserMessage(`
|
|
211
|
+
|
|
212
|
+
Here are the modification instructions.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
${modificationInstructions}
|
|
217
|
+
`);
|
|
218
|
+
convoBase.addDeveloperMessage(`
|
|
219
|
+
Before we begin, please provide a detailed plan outlining the specific steps you will take to
|
|
220
|
+
implement the requested modifications to the JSON object. This plan should break down the
|
|
221
|
+
modification instructions into a clear sequence of actions that you will perform, where each
|
|
222
|
+
action corresponds to a specific change in the JSON structure -- either the adding, removing,
|
|
223
|
+
or updating of specific individual properties or values.
|
|
224
|
+
|
|
225
|
+
Your response should start with "Modification Plan:" followed by the detailed plan.
|
|
226
|
+
`);
|
|
227
|
+
await convoBase.submit();
|
|
228
|
+
convoBase.addSystemMessage(`
|
|
229
|
+
NAVIGATING THE JSON OBJECT AND JSON PATHS
|
|
230
|
+
|
|
231
|
+
You have probably already noticed that the JSON object is annotated with placemarks
|
|
232
|
+
(comments) to indicate the positions of objects and indexes for better readability.
|
|
233
|
+
The syntax of these placemarks is quite straightforward, as it follows JavaScript/TypeScript
|
|
234
|
+
notation for accessing properties and array elements.
|
|
235
|
+
|
|
236
|
+
E.g. root["items"][0]["keywords"][1] refers to the second element of the "keywords" array
|
|
237
|
+
of the first element of the "items" array in the root object.
|
|
238
|
+
|
|
239
|
+
When prompted for a location in the JSON object, you'll emit a JSON list that corresponds
|
|
240
|
+
to the path to that location, where each element in the list is either a property name (string)
|
|
241
|
+
or an array index (number).
|
|
242
|
+
|
|
243
|
+
Thus, the path to root["items"][0]["keywords"][1] would be represented as:
|
|
244
|
+
json_path = ["items", 0, "keywords", 1]
|
|
245
|
+
`);
|
|
246
|
+
convoBase.addSystemMessage(`
|
|
247
|
+
INSTRUCTIONS FOR MODIFICATION OPERATIONS
|
|
248
|
+
|
|
249
|
+
You will be implementing the modification plan through a series of modification operations.
|
|
250
|
+
By incrementally applying these operations, we will arrive at the final modified JSON object that
|
|
251
|
+
satisfies the modification instructions.
|
|
252
|
+
|
|
253
|
+
We will be permitted to take multiple passes over the JSON object, and make multiple
|
|
254
|
+
modifications, so don't feel obligated to get everything right in the first few steps.
|
|
255
|
+
We will have plenty of opportunities to iteratively develop the final result. Your initial list
|
|
256
|
+
of operations to execute doesn't necessarily need to achieve the final result; if it merely
|
|
257
|
+
"walks" towards the final result, that's perfectly fine, as we'll be able to continue to "walk"
|
|
258
|
+
further towards the final result in subsequent iterations.
|
|
259
|
+
|
|
260
|
+
Structure of a Modification Operation:
|
|
261
|
+
|
|
262
|
+
- **json_path_of_parent**: A JSON path indicating the location in the JSON object where the
|
|
263
|
+
modification should be applied. We call this the "parent" location because we specify
|
|
264
|
+
the key or index within the parent later. For example, if you want to set the string
|
|
265
|
+
property "foo" to the value "bar" on the root object (i.e. root["foo"]="bar"), then
|
|
266
|
+
json_path_of_parent would be an empty list [], since the parent of "foo" is the root object.
|
|
267
|
+
If you want to append a new string value into the "keywords" array of the first
|
|
268
|
+
element of the "items" array (i.e. root["items"][0]["keywords"].push("new_keyword")), then
|
|
269
|
+
json_path_of_parent would be ["items", 0, "keywords"], since the parent of the new element
|
|
270
|
+
is the "keywords" array itself. The syntax of the json_path should follow the same notation
|
|
271
|
+
as described in the "Navigating the JSON Object and JSON Paths" section above.
|
|
272
|
+
|
|
273
|
+
- **key_or_index**: The key (string) or array index (number) of the property or element to modify.
|
|
274
|
+
If the parent location (json_path_of_parent) is an object, then this will be a string key.
|
|
275
|
+
If the parent location is an array, then this will be a numeric index.
|
|
276
|
+
SPECIAL: If you're using the "append" action (see below) to add a new element to the end of
|
|
277
|
+
an array, then key_or_index is ignored; set it to -1 to indicate to yourself that it's
|
|
278
|
+
irrelevant.
|
|
279
|
+
|
|
280
|
+
- **action**: The type of modification to perform. This can be one of the following values:
|
|
281
|
+
- "delete": Delete the specified property or array element. (The "data" field is ignored,
|
|
282
|
+
and should be set to null.) This "delete" action is functionally equivalent to
|
|
283
|
+
\`delete parent[key]\` for objects, or \`parent.splice(index, 1)\` for arrays.
|
|
284
|
+
- "assign": Set the property or element to a new value. If the parent is an array, then the
|
|
285
|
+
"assign" action will replace the existing element at the specified index. If the parent
|
|
286
|
+
is an object, then the "assign" action will set the value of the specified key, replacing
|
|
287
|
+
any existing value for that key if it already exists, or creating a new key-value pair if
|
|
288
|
+
the key does not already exist. This is functionally equivalent to:
|
|
289
|
+
\`parent[key] = data\` for objects, or
|
|
290
|
+
\`parent[index] = data\` for arrays.
|
|
291
|
+
- "append": Applicable only when the parent object is an array. Append a new element to the
|
|
292
|
+
end of the array. The "key_or_index" field is ignored. This is functionally equivalent to:
|
|
293
|
+
\`parent.push(data)\`.
|
|
294
|
+
- "insert": Applicable only when the parent object is an array. Insert a new element at the
|
|
295
|
+
specified index in the array. This is functionally equivalent to:
|
|
296
|
+
\`parent.splice(index, 0, data)\`.
|
|
297
|
+
- "rename": Applicable only when the parent object is an object. Rename a property from the
|
|
298
|
+
old key name (specified in "key_or_index") to a new key name. The "new_key_name" field
|
|
299
|
+
(see below) *must* be provided. The "data" field is ignored for this action; you should
|
|
300
|
+
just set it to null. This is functionally equivalent to:
|
|
301
|
+
\`parent[new_key_name] = parent[key_or_index]; delete parent[key_or_index]\`.
|
|
302
|
+
|
|
303
|
+
- **new_key_name**: Applicable only for the "rename" action. This field specifies the new key name
|
|
304
|
+
to rename a property to. For all other actions, this field is ignored and should just be set
|
|
305
|
+
to an empty string.
|
|
306
|
+
|
|
307
|
+
- **data**: The new value to set for "assign", "append", or "insert" actions. This field comes in
|
|
308
|
+
the following forms:
|
|
309
|
+
|
|
310
|
+
- **null**: You should set the data field to null when the action does not require a value,
|
|
311
|
+
such as "delete" or "rename". For all other actions, the data field cannot be null.
|
|
312
|
+
|
|
313
|
+
- **inline_value**: You explicitly write out the value to set. The value you construct will be
|
|
314
|
+
one of the following:
|
|
315
|
+
- A primitive value (string, number, boolean, null)
|
|
316
|
+
- An empty object ({}). This is useful for gradually building up complex objects through
|
|
317
|
+
a series of subsequent modifications.
|
|
318
|
+
- An empty array ([]). This is useful for gradually building up complex collections
|
|
319
|
+
through a series of subsequent modifications.
|
|
320
|
+
- An array whose elements are all either primitive values or empty objects/arrays. This is
|
|
321
|
+
useful for adding multiple related elements at once. If the array contains empty
|
|
322
|
+
objects/arrays, you can fill these in with actual values in subsequent modifications,
|
|
323
|
+
if needed.
|
|
324
|
+
- An object whose values are all either primitive values or empty objects/arrays. Again,
|
|
325
|
+
just like with arrays, if the object contains empty objects/arrays, you can fill these
|
|
326
|
+
in with actual values in subsequent modifications, if needed.
|
|
327
|
+
Note that "value" cannot recursively describe deeply nested structures in one step; it can
|
|
328
|
+
only describe at most a single layer of nesting. That's okay; you can build up complex
|
|
329
|
+
nested structures through a series of modifications, gradually filling in more and more
|
|
330
|
+
details with each modification. Just make sure to provide detailed notes about your plans
|
|
331
|
+
and intentions with each modification, so that we can keep track of the overall plan and
|
|
332
|
+
ensure that we're on the right track.
|
|
333
|
+
|
|
334
|
+
- **json_path_of_copy_source**: If you need to construct a deeply nested object, and there
|
|
335
|
+
happens to be a source object or array elsewhere in the JSON that already has a very
|
|
336
|
+
similar (or identical) structure, you can specify the JSON path to that source object
|
|
337
|
+
or array here. This can be a handy shortcut for creating complex structures without
|
|
338
|
+
having to manually specify every detail. PRO TIP: This is also a great way to **move**
|
|
339
|
+
existing structures around within the JSON, by copying from one path and then deleting
|
|
340
|
+
the original -- this makes moving a structure from one place to another a two-step process
|
|
341
|
+
instead of a long series of inline_value assignments.
|
|
342
|
+
`);
|
|
343
|
+
convoBase.addDeveloperMessage(`Alrighty then. Let's get to work!`);
|
|
344
|
+
const operationsDoneSoFar = [];
|
|
345
|
+
let operationLast = '(nothing, we just started)';
|
|
346
|
+
let operationNext = '(nothing, we just started)';
|
|
347
|
+
let operationLastWasSuccessful = true;
|
|
348
|
+
let numIterations = 0;
|
|
349
|
+
while (true) {
|
|
350
|
+
const convo = convoBase.clone();
|
|
351
|
+
numIterations++;
|
|
352
|
+
if (numIterations > 1) {
|
|
353
|
+
// If we have a work-in-progress callback, call it with the current state.
|
|
354
|
+
if (options?.onWorkInProgress) {
|
|
355
|
+
const objWipResult = await options.onWorkInProgress(JSON.parse(JSON.stringify(obj)));
|
|
356
|
+
if (objWipResult !== undefined) {
|
|
357
|
+
obj = JSON.parse(JSON.stringify(objWipResult));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const secondsElapsed = Math.floor((Date.now() - timeStarted) / 1000);
|
|
361
|
+
if (options?.giveUpAfterSeconds &&
|
|
362
|
+
secondsElapsed > options.giveUpAfterSeconds) {
|
|
363
|
+
throw new JSONSurgeryError(`Giving up after maximum time reached. ` +
|
|
364
|
+
`Seconds elapsed: ${secondsElapsed} ` +
|
|
365
|
+
`Maximum allowed: ${options.giveUpAfterSeconds}`, obj);
|
|
366
|
+
}
|
|
367
|
+
if (options?.giveUpAfterIterations &&
|
|
368
|
+
numIterations > options.giveUpAfterIterations) {
|
|
369
|
+
throw new JSONSurgeryError(`Giving up after ${options.giveUpAfterIterations}. Maximum iterations reached. `, obj);
|
|
370
|
+
}
|
|
371
|
+
convo.addUserMessage(`
|
|
372
|
+
CURRENT STATUS:
|
|
373
|
+
We've been processing for ${secondsElapsed} seconds.
|
|
374
|
+
We've performed ${operationsDoneSoFar.length} operations across ${numIterations - 1} iterations.
|
|
375
|
+
`);
|
|
376
|
+
if (operationsDoneSoFar.length > 0) {
|
|
377
|
+
let msgOpsSoFar = ``;
|
|
378
|
+
for (let i = 0; i < operationsDoneSoFar.length; i++) {
|
|
379
|
+
msgOpsSoFar += `${i + 1}. ${operationsDoneSoFar[i]}\n`;
|
|
380
|
+
}
|
|
381
|
+
convo.addUserMessage(`
|
|
382
|
+
Here are the operations that we've performed so far:
|
|
383
|
+
${msgOpsSoFar}
|
|
384
|
+
`);
|
|
385
|
+
}
|
|
386
|
+
convo.addUserMessage({
|
|
387
|
+
role: 'user',
|
|
388
|
+
content: `
|
|
389
|
+
Here is the JSON object in its current state, with our modifications up to this point applied.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
${placemarkedJSONStringify(obj, 2, options.skippedKeys)}
|
|
394
|
+
`,
|
|
395
|
+
});
|
|
396
|
+
convo.addUserMessage({
|
|
397
|
+
role: 'user',
|
|
398
|
+
content: `
|
|
399
|
+
Just to help re-establish context from previous iterations, here is the last operation we
|
|
400
|
+
performed on this JSON object:
|
|
401
|
+
${operationLast}
|
|
402
|
+
|
|
403
|
+
Here's what we were planning to do next:
|
|
404
|
+
${operationNext}
|
|
405
|
+
`,
|
|
406
|
+
});
|
|
407
|
+
if (!operationLastWasSuccessful) {
|
|
408
|
+
convo.addDeveloperMessage({
|
|
409
|
+
role: 'user',
|
|
410
|
+
content: `
|
|
411
|
+
!CRITICAL: The last operation we attempted FAILED! DO NOT DO THE SAME OPERATION AGAIN!
|
|
412
|
+
Think of a *different* operation to perform. Think of a different *way* to perform
|
|
413
|
+
the modifications that we need to make. Break them up over different steps, or
|
|
414
|
+
re-think your approach, or SOMETHING. Just DO NOT keep trying to do the same thing
|
|
415
|
+
over and over again expecting a different result.
|
|
416
|
+
`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
convo.addDeveloperMessage(`
|
|
421
|
+
Refer to the "Instructions for Modification Operations" section above for the
|
|
422
|
+
structure of a modification operation.
|
|
423
|
+
|
|
424
|
+
Based on the overall modification plan and the current state of the JSON object,
|
|
425
|
+
determine and describe the next modifications to apply to the JSON object. Your
|
|
426
|
+
output for now should just be plain English. Later, when I ask you to, you'll
|
|
427
|
+
formalize it into a JSON object -- but for now, just talk your way through it.
|
|
428
|
+
|
|
429
|
+
If the modification instructions require multiple changes to the JSON object, you can describe
|
|
430
|
+
multiple modifications to apply in this step. You can also break down a complex modification into
|
|
431
|
+
a series of simpler modifications that can be applied incrementally, if that makes it easier to
|
|
432
|
+
implement the overall modification instructions correctly. Just make sure to be very clear and
|
|
433
|
+
detailed in describing your intended modifications, so that we can ensure that we're on the
|
|
434
|
+
right track and that the modifications you propose are correctly implementing the modification
|
|
435
|
+
instructions.
|
|
436
|
+
|
|
437
|
+
If the modification instructions have already been fully satisfied and no further modifications
|
|
438
|
+
are needed, then just say that we're done and don't propose any further modifications.
|
|
439
|
+
`);
|
|
440
|
+
await convo.submit();
|
|
441
|
+
await convo.submit(undefined, undefined, {
|
|
442
|
+
jsonResponse: {
|
|
443
|
+
format: {
|
|
444
|
+
type: 'json_schema',
|
|
445
|
+
name: 'json_object_modifications',
|
|
446
|
+
description: `
|
|
447
|
+
A JSON formalization of the next set of modifications to apply to the JSON object,
|
|
448
|
+
as we have just determined and described. If there are multiple modifications to apply,
|
|
449
|
+
you can include all of them in this JSON object. If there are no modifications to apply
|
|
450
|
+
because the modification instructions have already been fully satisfied, then set the
|
|
451
|
+
"modifications" field to an empty list.
|
|
452
|
+
`,
|
|
453
|
+
schema: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
modifications: {
|
|
457
|
+
type: 'array',
|
|
458
|
+
items: {
|
|
459
|
+
type: 'object',
|
|
460
|
+
properties: {
|
|
461
|
+
json_path_of_parent: JSON_SCHEMA_JSON_PATH,
|
|
462
|
+
key_or_index: {
|
|
463
|
+
anyOf: [{ type: 'string' }, { type: 'number' }],
|
|
464
|
+
},
|
|
465
|
+
action: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
enum: ['assign', 'delete', 'append', 'insert', 'rename'],
|
|
468
|
+
},
|
|
469
|
+
new_key_name: { type: 'string' },
|
|
470
|
+
data: {
|
|
471
|
+
anyOf: [
|
|
472
|
+
{
|
|
473
|
+
type: 'null',
|
|
474
|
+
description: 'Data field should be null for "delete" and "rename" actions.',
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
inline_value: {
|
|
480
|
+
...JSON_SCHEMA_SET_VALUE,
|
|
481
|
+
description: `
|
|
482
|
+
The new value to set for "assign", "append", or "insert" actions.
|
|
483
|
+
For "delete" and "rename" actions, this field is ignored and should be set to null.
|
|
484
|
+
This can be one of the following:
|
|
485
|
+
- A primitive value (string, number, boolean, null)
|
|
486
|
+
- An empty object ({}).
|
|
487
|
+
- An empty array ([]).
|
|
488
|
+
- An array whose elements are all either primitive values or empty objects/arrays.
|
|
489
|
+
- An object(*) whose values are all either primitive values or empty objects/arrays.
|
|
490
|
+
(*) NOTE: Due to some limitations in our JSON schema processor, we cannot have you
|
|
491
|
+
provide an object with arbitrary keys directly. Instead, please provide an array of
|
|
492
|
+
key-value pair objects. I know it *looks like* an array, but we'll interpret it as
|
|
493
|
+
an object.
|
|
494
|
+
`,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
required: ['inline_value'],
|
|
498
|
+
additionalProperties: false,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
json_path_of_copy_source: JSON_SCHEMA_JSON_PATH,
|
|
504
|
+
},
|
|
505
|
+
required: ['json_path_of_copy_source'],
|
|
506
|
+
additionalProperties: false,
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
required: [
|
|
512
|
+
'json_path_of_parent',
|
|
513
|
+
'key_or_index',
|
|
514
|
+
'action',
|
|
515
|
+
'new_key_name',
|
|
516
|
+
'data',
|
|
517
|
+
],
|
|
518
|
+
additionalProperties: false,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
required: ['modifications'],
|
|
523
|
+
additionalProperties: false,
|
|
524
|
+
},
|
|
525
|
+
strict: true,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
const modifications = convo.getLastReplyDictField('modifications');
|
|
530
|
+
if (!modifications || modifications.length === 0) {
|
|
531
|
+
// No modifications proposed.
|
|
532
|
+
// Presumably this means that all modification instructions have been fully satisfied.
|
|
533
|
+
// We might be done!
|
|
534
|
+
let validationErrors = [];
|
|
535
|
+
if (options.onValidateBeforeReturn) {
|
|
536
|
+
const validationResult = await options.onValidateBeforeReturn(JSON.parse(JSON.stringify(obj)));
|
|
537
|
+
if (validationResult) {
|
|
538
|
+
if (validationResult.objCorrected) {
|
|
539
|
+
// The validation function corrected the object.
|
|
540
|
+
// (Maybe it *also* found errors, but it corrected what it could.)
|
|
541
|
+
obj = validationResult.objCorrected;
|
|
542
|
+
}
|
|
543
|
+
if (validationResult.errors && validationResult.errors.length > 0) {
|
|
544
|
+
// The validation function found errors in the object.
|
|
545
|
+
// (Maybe it *also* corrected what it could, but these errors remain.)
|
|
546
|
+
validationErrors = validationResult.errors;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (validationErrors.length === 0) {
|
|
551
|
+
return obj;
|
|
552
|
+
}
|
|
553
|
+
// Kick it back for further processing.
|
|
554
|
+
operationLastWasSuccessful = false;
|
|
555
|
+
let actionDesc = `
|
|
556
|
+
ERROR: Validation failure on attempted exit.
|
|
557
|
+
We thought we had finished processing, but the object didn't pass an automated
|
|
558
|
+
validation check. This is often the result of undocumented requirements,
|
|
559
|
+
and isn't necessarily because you did anything wrong. Nonetheless, these remaining
|
|
560
|
+
issues must be addressed before we can consider the object valid.)
|
|
561
|
+
|
|
562
|
+
The validator returned the following errors:
|
|
563
|
+
|
|
564
|
+
- ${validationErrors.join('\n -')}
|
|
565
|
+
`;
|
|
566
|
+
operationsDoneSoFar.push(actionDesc);
|
|
567
|
+
operationLast = actionDesc;
|
|
568
|
+
operationNext = `Fix these validation errors, and try to exit again when they're done.`;
|
|
569
|
+
// NOTE: We might want to submit an LLM call here to get more detailed guidance
|
|
570
|
+
// for operationNext, but in practice that hasn't been necessary. Let's not fix
|
|
571
|
+
// something that isn't broken, and let's not burn tokens when a canned response
|
|
572
|
+
// suffices.
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const objModified = JSON.parse(JSON.stringify(obj));
|
|
576
|
+
for (const modification of modifications) {
|
|
577
|
+
try {
|
|
578
|
+
let { json_path_of_parent, key_or_index, action, new_key_name, data } = modification;
|
|
579
|
+
let value = null;
|
|
580
|
+
if (data) {
|
|
581
|
+
if (data.inline_value) {
|
|
582
|
+
// We're kinda abusing TypeScript's leniency with the "any" type here.
|
|
583
|
+
value = unpackValueFromSetValueSchema(data.inline_value);
|
|
584
|
+
}
|
|
585
|
+
else if (data.json_path_of_copy_source) {
|
|
586
|
+
// Handle the case where the value is a reference to another part of the JSON object.
|
|
587
|
+
const sourcePath = data.json_path_of_copy_source;
|
|
588
|
+
const sourceNavResult = navigateToJSONPath(objModified, sourcePath);
|
|
589
|
+
value = JSON.parse(JSON.stringify(sourceNavResult.pathTarget));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!json_path_of_parent) {
|
|
593
|
+
throw new Error(`Missing required field "json_path_of_parent" in modification.`);
|
|
594
|
+
}
|
|
595
|
+
if (!action) {
|
|
596
|
+
throw new Error(`Missing required field "action" in modification.`);
|
|
597
|
+
}
|
|
598
|
+
if (action === 'rename' && !new_key_name) {
|
|
599
|
+
throw new Error(`Missing required field "new_key_name" for "rename" action in modification.`);
|
|
600
|
+
}
|
|
601
|
+
const jsonPathNavResult = navigateToJSONPath(objModified, json_path_of_parent);
|
|
602
|
+
const targetParent = jsonPathNavResult.pathTarget;
|
|
603
|
+
// targetParent must be either an object or an array. It cannot be
|
|
604
|
+
// null, undefined, or a primitive value.
|
|
605
|
+
if (!targetParent ||
|
|
606
|
+
!(typeof targetParent === 'object' || Array.isArray(targetParent))) {
|
|
607
|
+
throw new Error(`Error: json_path_of_parent points to something that is neither an object ` +
|
|
608
|
+
`nor an array. It must point to either an object or an array. Instead, ` +
|
|
609
|
+
`${json_path_of_parent} points to the value: ${JSON.stringify(targetParent)}`);
|
|
610
|
+
}
|
|
611
|
+
// Make sure that targetParent is an array if the action is an array-specific action,
|
|
612
|
+
// and is an object if the action is an object-specific action.
|
|
613
|
+
if (action === 'assign') {
|
|
614
|
+
targetParent[key_or_index] = value;
|
|
615
|
+
}
|
|
616
|
+
else if (action === 'delete') {
|
|
617
|
+
if (Array.isArray(targetParent)) {
|
|
618
|
+
targetParent.splice(key_or_index, 1);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
delete targetParent[key_or_index];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (action === 'append') {
|
|
625
|
+
if (!Array.isArray(targetParent)) {
|
|
626
|
+
throw new Error(`Error: "append" action can only be applied to arrays. However, ` +
|
|
627
|
+
`the target location specified by json_path_of_parent points to a non-array value: ` +
|
|
628
|
+
`${JSON.stringify(targetParent)}`);
|
|
629
|
+
}
|
|
630
|
+
targetParent.push(value);
|
|
631
|
+
}
|
|
632
|
+
else if (action === 'insert') {
|
|
633
|
+
if (!Array.isArray(targetParent)) {
|
|
634
|
+
throw new Error(`Error: "insert" action can only be applied to arrays. However, ` +
|
|
635
|
+
`the target location specified by json_path_of_parent points to a non-array value: ` +
|
|
636
|
+
`${JSON.stringify(targetParent)}`);
|
|
637
|
+
}
|
|
638
|
+
targetParent.splice(key_or_index, 0, value);
|
|
639
|
+
}
|
|
640
|
+
else if (action === 'rename') {
|
|
641
|
+
if (Array.isArray(targetParent)) {
|
|
642
|
+
throw new Error(`Error: "rename" action can only be applied to objects. However, ` +
|
|
643
|
+
`the target location specified by json_path_of_parent points to an array value: ` +
|
|
644
|
+
`${JSON.stringify(targetParent)}`);
|
|
645
|
+
}
|
|
646
|
+
targetParent[new_key_name] = targetParent[key_or_index];
|
|
647
|
+
delete targetParent[key_or_index];
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
throw new Error(`Unknown action: ${action}`);
|
|
651
|
+
}
|
|
652
|
+
convo.addSystemMessage(`
|
|
653
|
+
The following modification was applied to the JSON object without any errors:
|
|
654
|
+
|
|
655
|
+
json_path_of_parent: ${JSON.stringify(json_path_of_parent)}
|
|
656
|
+
key_or_index: ${JSON.stringify(key_or_index)}
|
|
657
|
+
action: ${JSON.stringify(action)}
|
|
658
|
+
new_key_name: ${JSON.stringify(new_key_name)}
|
|
659
|
+
value: ${JSON.stringify(value, null, 2)}
|
|
660
|
+
`);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
convo.addSystemMessage(`
|
|
664
|
+
An error occurred while attempting to apply the proposed modification:
|
|
665
|
+
${error.message}
|
|
666
|
+
`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Verify the modified object with the LLM
|
|
670
|
+
convo.addUserMessage(`
|
|
671
|
+
Here is the JSON object after applying the proposed modification.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
${placemarkedJSONStringify(objModified, 2, options.skippedKeys)}
|
|
676
|
+
`);
|
|
677
|
+
convo.addDeveloperMessage(`
|
|
678
|
+
Examining the modified JSON object after the proposed modifications have been applied,
|
|
679
|
+
let's discuss and analyze whether or not these modifications are correct, i.e. whether
|
|
680
|
+
or not they properly implement the intended changes.
|
|
681
|
+
|
|
682
|
+
Specifically, check the following:
|
|
683
|
+
|
|
684
|
+
- Does the modified JSON object now correctly contain the modifications?
|
|
685
|
+
|
|
686
|
+
- Does the modified JSON object contain any unintended changes? This typically results from
|
|
687
|
+
one or more poorly structured modification objects, where json_path_of_parent point to
|
|
688
|
+
the wrong location in the JSON object.
|
|
689
|
+
|
|
690
|
+
- Are the modifications *correct but incomplete*? In other words, were the modifications
|
|
691
|
+
that were applied correct in the sense that they were consistent with the modification
|
|
692
|
+
instructions, but they only represent one step in a multi-step process? If so, then this
|
|
693
|
+
is not a problem at all. We will continue to apply more modifications in subsequent
|
|
694
|
+
iterations.
|
|
695
|
+
|
|
696
|
+
At the end of your analysis, write a conclusion. Your conclusion should be one of the
|
|
697
|
+
following, or some variant thereof:
|
|
698
|
+
|
|
699
|
+
- The changes are correct and complete for this step in the modification process.
|
|
700
|
+
We can keep them, and we can move on to the next step in the modification process.
|
|
701
|
+
|
|
702
|
+
- The changes are correct but incomplete for this step in the modification process.
|
|
703
|
+
They are consistent with the modification instructions, but they only represent one step
|
|
704
|
+
in a multi-step process. This is not a problem at all. We will keep these changes,
|
|
705
|
+
and we will continue to apply more modifications in subsequent iterations,
|
|
706
|
+
to make further progress towards satisfying the modification instructions.
|
|
707
|
+
|
|
708
|
+
- The changes are incorrect for this step in the modification process. They do not progress
|
|
709
|
+
us towards satisfying the modification instructions, and they may even break things and
|
|
710
|
+
take us further away from satisfying the modification instructions. We should reject these
|
|
711
|
+
changes, revert back to the previous version of the JSON object, and try a different
|
|
712
|
+
modification operation that is more likely to be correct.
|
|
713
|
+
`);
|
|
714
|
+
await convo.submit();
|
|
715
|
+
await convo.submit(undefined, undefined, {
|
|
716
|
+
jsonResponse: {
|
|
717
|
+
format: {
|
|
718
|
+
type: 'json_schema',
|
|
719
|
+
name: 'modification_verification',
|
|
720
|
+
description: `
|
|
721
|
+
A JSON formalization of the analysis and verification of the modifications that were just applied
|
|
722
|
+
to the JSON object, as we have just discussed and analyzed.
|
|
723
|
+
`,
|
|
724
|
+
schema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
description_of_changes_intended: { type: 'string' },
|
|
728
|
+
description_of_changes_applied: { type: 'string' },
|
|
729
|
+
should_we_keep_these_changes: { type: 'boolean' },
|
|
730
|
+
reason_to_revert: {
|
|
731
|
+
type: 'string',
|
|
732
|
+
description: `If should_we_keep_these_changes is false, provide a brief explanation ` +
|
|
733
|
+
`of why one or more modifications were incorrect. ` +
|
|
734
|
+
`If should_we_keep_these_changes is true, ` +
|
|
735
|
+
`this field should be an empty string.`,
|
|
736
|
+
},
|
|
737
|
+
next_step: {
|
|
738
|
+
type: 'string',
|
|
739
|
+
description: `A brief discussion of the next step to take in the modification process, ` +
|
|
740
|
+
`to make further progress towards satisfying the modification instructions. ` +
|
|
741
|
+
`If the modifications that were just applied already fully satisfy the ` +
|
|
742
|
+
`modification instructions and no further modifications are needed, ` +
|
|
743
|
+
`then just say so.`,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
required: [
|
|
747
|
+
'description_of_changes_intended',
|
|
748
|
+
'description_of_changes_applied',
|
|
749
|
+
'should_we_keep_these_changes',
|
|
750
|
+
'reason_to_revert',
|
|
751
|
+
'next_step',
|
|
752
|
+
],
|
|
753
|
+
additionalProperties: false,
|
|
754
|
+
},
|
|
755
|
+
strict: true,
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
let actionDesc = `DONE: ${convo.getLastReplyDictField('description_of_changes_applied')}`;
|
|
760
|
+
operationLastWasSuccessful = true;
|
|
761
|
+
if (convo.getLastReplyDictField('should_we_keep_these_changes')) {
|
|
762
|
+
// Accept the modification
|
|
763
|
+
obj = objModified;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Reject the modification
|
|
767
|
+
actionDesc =
|
|
768
|
+
`FAILED: ${convo.getLastReplyDictField('description_of_changes_intended')}` +
|
|
769
|
+
` Reason for failure: ${convo.getLastReplyDictField('reason_to_revert')}`;
|
|
770
|
+
operationLastWasSuccessful = false;
|
|
771
|
+
}
|
|
772
|
+
operationsDoneSoFar.push(actionDesc);
|
|
773
|
+
operationLast = actionDesc;
|
|
774
|
+
operationNext = `${convo.getLastReplyDictField('next_step')}`;
|
|
775
|
+
}
|
|
776
|
+
};
|