@fluidframework/ai-collab 2.10.0-306579

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.
Files changed (168) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/CHANGELOG.md +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +280 -0
  5. package/alpha.d.ts +11 -0
  6. package/api-extractor/api-extractor-lint-alpha.cjs.json +5 -0
  7. package/api-extractor/api-extractor-lint-alpha.esm.json +5 -0
  8. package/api-extractor/api-extractor-lint-bundle.json +5 -0
  9. package/api-extractor/api-extractor-lint-index.cjs.json +5 -0
  10. package/api-extractor/api-extractor-lint-index.esm.json +5 -0
  11. package/api-extractor/api-extractor-lint-public.cjs.json +5 -0
  12. package/api-extractor/api-extractor-lint-public.esm.json +5 -0
  13. package/api-extractor-lint.json +4 -0
  14. package/api-extractor.json +4 -0
  15. package/api-report/ai-collab.alpha.api.md +164 -0
  16. package/api-report/ai-collab.beta.api.md +7 -0
  17. package/api-report/ai-collab.public.api.md +7 -0
  18. package/biome.jsonc +4 -0
  19. package/dist/aiCollab.d.ts +65 -0
  20. package/dist/aiCollab.d.ts.map +1 -0
  21. package/dist/aiCollab.js +81 -0
  22. package/dist/aiCollab.js.map +1 -0
  23. package/dist/aiCollabApi.d.ts +173 -0
  24. package/dist/aiCollabApi.d.ts.map +1 -0
  25. package/dist/aiCollabApi.js +7 -0
  26. package/dist/aiCollabApi.js.map +1 -0
  27. package/dist/alpha.d.ts +41 -0
  28. package/dist/explicit-strategy/agentEditReducer.d.ts +12 -0
  29. package/dist/explicit-strategy/agentEditReducer.d.ts.map +1 -0
  30. package/dist/explicit-strategy/agentEditReducer.js +394 -0
  31. package/dist/explicit-strategy/agentEditReducer.js.map +1 -0
  32. package/dist/explicit-strategy/agentEditTypes.d.ts +158 -0
  33. package/dist/explicit-strategy/agentEditTypes.d.ts.map +1 -0
  34. package/dist/explicit-strategy/agentEditTypes.js +50 -0
  35. package/dist/explicit-strategy/agentEditTypes.js.map +1 -0
  36. package/dist/explicit-strategy/idGenerator.d.ts +22 -0
  37. package/dist/explicit-strategy/idGenerator.d.ts.map +1 -0
  38. package/dist/explicit-strategy/idGenerator.js +74 -0
  39. package/dist/explicit-strategy/idGenerator.js.map +1 -0
  40. package/dist/explicit-strategy/index.d.ts +51 -0
  41. package/dist/explicit-strategy/index.d.ts.map +1 -0
  42. package/dist/explicit-strategy/index.js +223 -0
  43. package/dist/explicit-strategy/index.js.map +1 -0
  44. package/dist/explicit-strategy/jsonTypes.d.ts +23 -0
  45. package/dist/explicit-strategy/jsonTypes.d.ts.map +1 -0
  46. package/dist/explicit-strategy/jsonTypes.js +7 -0
  47. package/dist/explicit-strategy/jsonTypes.js.map +1 -0
  48. package/dist/explicit-strategy/promptGeneration.d.ts +51 -0
  49. package/dist/explicit-strategy/promptGeneration.d.ts.map +1 -0
  50. package/dist/explicit-strategy/promptGeneration.js +218 -0
  51. package/dist/explicit-strategy/promptGeneration.js.map +1 -0
  52. package/dist/explicit-strategy/typeGeneration.d.ts +15 -0
  53. package/dist/explicit-strategy/typeGeneration.d.ts.map +1 -0
  54. package/dist/explicit-strategy/typeGeneration.js +264 -0
  55. package/dist/explicit-strategy/typeGeneration.js.map +1 -0
  56. package/dist/explicit-strategy/utils.d.ts +37 -0
  57. package/dist/explicit-strategy/utils.d.ts.map +1 -0
  58. package/dist/explicit-strategy/utils.js +47 -0
  59. package/dist/explicit-strategy/utils.js.map +1 -0
  60. package/dist/implicit-strategy/index.d.ts +8 -0
  61. package/dist/implicit-strategy/index.d.ts.map +1 -0
  62. package/dist/implicit-strategy/index.js +18 -0
  63. package/dist/implicit-strategy/index.js.map +1 -0
  64. package/dist/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
  65. package/dist/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
  66. package/dist/implicit-strategy/sharedTreeBranchManager.js +212 -0
  67. package/dist/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
  68. package/dist/implicit-strategy/sharedTreeDiff.d.ts +102 -0
  69. package/dist/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
  70. package/dist/implicit-strategy/sharedTreeDiff.js +522 -0
  71. package/dist/implicit-strategy/sharedTreeDiff.js.map +1 -0
  72. package/dist/implicit-strategy/utils.d.ts +21 -0
  73. package/dist/implicit-strategy/utils.d.ts.map +1 -0
  74. package/dist/implicit-strategy/utils.js +49 -0
  75. package/dist/implicit-strategy/utils.js.map +1 -0
  76. package/dist/index.d.ts +16 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +24 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/package.json +3 -0
  81. package/dist/public.d.ts +19 -0
  82. package/eslintrc.cjs +11 -0
  83. package/internal.d.ts +11 -0
  84. package/lib/aiCollab.d.ts +65 -0
  85. package/lib/aiCollab.d.ts.map +1 -0
  86. package/lib/aiCollab.js +77 -0
  87. package/lib/aiCollab.js.map +1 -0
  88. package/lib/aiCollabApi.d.ts +173 -0
  89. package/lib/aiCollabApi.d.ts.map +1 -0
  90. package/lib/aiCollabApi.js +6 -0
  91. package/lib/aiCollabApi.js.map +1 -0
  92. package/lib/alpha.d.ts +41 -0
  93. package/lib/explicit-strategy/agentEditReducer.d.ts +12 -0
  94. package/lib/explicit-strategy/agentEditReducer.d.ts.map +1 -0
  95. package/lib/explicit-strategy/agentEditReducer.js +390 -0
  96. package/lib/explicit-strategy/agentEditReducer.js.map +1 -0
  97. package/lib/explicit-strategy/agentEditTypes.d.ts +158 -0
  98. package/lib/explicit-strategy/agentEditTypes.d.ts.map +1 -0
  99. package/lib/explicit-strategy/agentEditTypes.js +47 -0
  100. package/lib/explicit-strategy/agentEditTypes.js.map +1 -0
  101. package/lib/explicit-strategy/idGenerator.d.ts +22 -0
  102. package/lib/explicit-strategy/idGenerator.d.ts.map +1 -0
  103. package/lib/explicit-strategy/idGenerator.js +70 -0
  104. package/lib/explicit-strategy/idGenerator.js.map +1 -0
  105. package/lib/explicit-strategy/index.d.ts +51 -0
  106. package/lib/explicit-strategy/index.d.ts.map +1 -0
  107. package/lib/explicit-strategy/index.js +219 -0
  108. package/lib/explicit-strategy/index.js.map +1 -0
  109. package/lib/explicit-strategy/jsonTypes.d.ts +23 -0
  110. package/lib/explicit-strategy/jsonTypes.d.ts.map +1 -0
  111. package/lib/explicit-strategy/jsonTypes.js +6 -0
  112. package/lib/explicit-strategy/jsonTypes.js.map +1 -0
  113. package/lib/explicit-strategy/promptGeneration.d.ts +51 -0
  114. package/lib/explicit-strategy/promptGeneration.d.ts.map +1 -0
  115. package/lib/explicit-strategy/promptGeneration.js +208 -0
  116. package/lib/explicit-strategy/promptGeneration.js.map +1 -0
  117. package/lib/explicit-strategy/typeGeneration.d.ts +15 -0
  118. package/lib/explicit-strategy/typeGeneration.d.ts.map +1 -0
  119. package/lib/explicit-strategy/typeGeneration.js +260 -0
  120. package/lib/explicit-strategy/typeGeneration.js.map +1 -0
  121. package/lib/explicit-strategy/utils.d.ts +37 -0
  122. package/lib/explicit-strategy/utils.d.ts.map +1 -0
  123. package/lib/explicit-strategy/utils.js +41 -0
  124. package/lib/explicit-strategy/utils.js.map +1 -0
  125. package/lib/implicit-strategy/index.d.ts +8 -0
  126. package/lib/implicit-strategy/index.d.ts.map +1 -0
  127. package/lib/implicit-strategy/index.js +8 -0
  128. package/lib/implicit-strategy/index.js.map +1 -0
  129. package/lib/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
  130. package/lib/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
  131. package/lib/implicit-strategy/sharedTreeBranchManager.js +213 -0
  132. package/lib/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
  133. package/lib/implicit-strategy/sharedTreeDiff.d.ts +102 -0
  134. package/lib/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
  135. package/lib/implicit-strategy/sharedTreeDiff.js +515 -0
  136. package/lib/implicit-strategy/sharedTreeDiff.js.map +1 -0
  137. package/lib/implicit-strategy/utils.d.ts +21 -0
  138. package/lib/implicit-strategy/utils.d.ts.map +1 -0
  139. package/lib/implicit-strategy/utils.js +43 -0
  140. package/lib/implicit-strategy/utils.js.map +1 -0
  141. package/lib/index.d.ts +16 -0
  142. package/lib/index.d.ts.map +1 -0
  143. package/lib/index.js +15 -0
  144. package/lib/index.js.map +1 -0
  145. package/lib/public.d.ts +19 -0
  146. package/lib/tsdoc-metadata.json +11 -0
  147. package/mocharc.cjs +14 -0
  148. package/package.json +165 -0
  149. package/prettier.config.cjs +8 -0
  150. package/src/aiCollab.ts +86 -0
  151. package/src/aiCollabApi.ts +184 -0
  152. package/src/explicit-strategy/agentEditReducer.ts +498 -0
  153. package/src/explicit-strategy/agentEditTypes.ts +177 -0
  154. package/src/explicit-strategy/idGenerator.ts +90 -0
  155. package/src/explicit-strategy/index.ts +364 -0
  156. package/src/explicit-strategy/jsonTypes.ts +27 -0
  157. package/src/explicit-strategy/promptGeneration.ts +294 -0
  158. package/src/explicit-strategy/typeGeneration.ts +374 -0
  159. package/src/explicit-strategy/utils.ts +60 -0
  160. package/src/implicit-strategy/README.md +4 -0
  161. package/src/implicit-strategy/index.ts +21 -0
  162. package/src/implicit-strategy/sharedTreeBranchManager.ts +294 -0
  163. package/src/implicit-strategy/sharedTreeDiff.ts +735 -0
  164. package/src/implicit-strategy/utils.ts +54 -0
  165. package/src/index.ts +39 -0
  166. package/tsconfig.cjs.json +7 -0
  167. package/tsconfig.json +12 -0
  168. package/tsdoc.json +4 -0
@@ -0,0 +1,498 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { assert } from "@fluidframework/core-utils/internal";
7
+ import { isFluidHandle } from "@fluidframework/runtime-utils";
8
+ import { UsageError } from "@fluidframework/telemetry-utils/internal";
9
+ import {
10
+ Tree,
11
+ NodeKind,
12
+ type ImplicitAllowedTypes,
13
+ type TreeArrayNode,
14
+ type TreeNode,
15
+ type TreeNodeSchema,
16
+ type SimpleNodeSchema,
17
+ FieldKind,
18
+ FieldSchema,
19
+ normalizeAllowedTypes,
20
+ type ImplicitFieldSchema,
21
+ booleanSchema,
22
+ handleSchema,
23
+ nullSchema,
24
+ numberSchema,
25
+ stringSchema,
26
+ type IterableTreeArrayContent,
27
+ } from "@fluidframework/tree/internal";
28
+
29
+ import {
30
+ type TreeEdit,
31
+ type ObjectTarget,
32
+ type Selection,
33
+ type Range,
34
+ type ObjectPlace,
35
+ type ArrayPlace,
36
+ type TreeEditObject,
37
+ type TreeEditValue,
38
+ typeField,
39
+ } from "./agentEditTypes.js";
40
+ import type { IdGenerator } from "./idGenerator.js";
41
+ import type { JsonValue } from "./jsonTypes.js";
42
+ import { toDecoratedJson } from "./promptGeneration.js";
43
+ import { fail } from "./utils.js";
44
+
45
+ function populateDefaults(
46
+ json: JsonValue,
47
+ definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
48
+ ): void {
49
+ if (typeof json === "object") {
50
+ if (json === null) {
51
+ return;
52
+ }
53
+ if (Array.isArray(json)) {
54
+ for (const element of json) {
55
+ populateDefaults(element, definitionMap);
56
+ }
57
+ } else {
58
+ assert(
59
+ typeof json[typeField] === "string",
60
+ "The typeField must be present in new JSON content",
61
+ );
62
+ const nodeSchema = definitionMap.get(json[typeField]);
63
+ assert(nodeSchema?.kind === NodeKind.Object, "Expected object schema");
64
+ }
65
+ }
66
+ }
67
+
68
+ function getSchemaIdentifier(content: TreeEditValue): string | undefined {
69
+ switch (typeof content) {
70
+ case "boolean": {
71
+ return booleanSchema.identifier;
72
+ }
73
+ case "number": {
74
+ return numberSchema.identifier;
75
+ }
76
+ case "string": {
77
+ return stringSchema.identifier;
78
+ }
79
+ case "object": {
80
+ if (content === null) {
81
+ return nullSchema.identifier;
82
+ }
83
+ if (Array.isArray(content)) {
84
+ throw new UsageError("Arrays are not currently supported in this context");
85
+ }
86
+ if (isFluidHandle(content)) {
87
+ return handleSchema.identifier;
88
+ }
89
+ return content[typeField];
90
+ }
91
+ default: {
92
+ throw new UsageError("Unsupported content type");
93
+ }
94
+ }
95
+ }
96
+
97
+ function contentWithIds(content: TreeNode, idGenerator: IdGenerator): TreeEditObject {
98
+ return JSON.parse(toDecoratedJson(idGenerator, content)) as TreeEditObject;
99
+ }
100
+
101
+ /**
102
+ * Manages applying the various types of {@link TreeEdit}'s to a a given {@link TreeNode}.
103
+ */
104
+ export function applyAgentEdit(
105
+ treeEdit: TreeEdit,
106
+ idGenerator: IdGenerator,
107
+ definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
108
+ validator?: (edit: TreeNode) => void,
109
+ ): TreeEdit {
110
+ assertObjectIdsExist(treeEdit, idGenerator);
111
+ switch (treeEdit.type) {
112
+ case "insert": {
113
+ const { array, index } = getPlaceInfo(treeEdit.destination, idGenerator);
114
+
115
+ const parentNodeSchema = Tree.schema(array);
116
+ populateDefaults(treeEdit.content, definitionMap);
117
+
118
+ const schemaIdentifier = getSchemaIdentifier(treeEdit.content);
119
+
120
+ // We assume that the parentNode for inserts edits are guaranteed to be an arrayNode.
121
+ const allowedTypes = [
122
+ ...normalizeAllowedTypes(parentNodeSchema.info as ImplicitAllowedTypes),
123
+ ];
124
+
125
+ for (const allowedType of allowedTypes.values()) {
126
+ if (allowedType.identifier === schemaIdentifier && typeof allowedType === "function") {
127
+ const simpleNodeSchema = allowedType as unknown as new (dummy: unknown) => TreeNode;
128
+ const insertNode = new simpleNodeSchema(treeEdit.content);
129
+ validator?.(insertNode);
130
+ array.insertAt(index, insertNode as unknown as IterableTreeArrayContent<never>);
131
+ return {
132
+ ...treeEdit,
133
+ content: contentWithIds(insertNode, idGenerator),
134
+ };
135
+ }
136
+ }
137
+ fail("inserted node must be of an allowed type");
138
+ }
139
+ case "remove": {
140
+ const source = treeEdit.source;
141
+ if (isObjectTarget(source)) {
142
+ const node = getNodeFromTarget(source, idGenerator);
143
+ const parentNode = Tree.parent(node);
144
+ // Case for deleting rootNode
145
+ if (parentNode === undefined) {
146
+ throw new UsageError(
147
+ "The root is required, and cannot be removed. Please use modify edit instead.",
148
+ );
149
+ } else if (Tree.schema(parentNode).kind === NodeKind.Array) {
150
+ const nodeIndex = Tree.key(node) as number;
151
+ (parentNode as TreeArrayNode).removeAt(nodeIndex);
152
+ } else {
153
+ const fieldKey = Tree.key(node);
154
+ const parentSchema = Tree.schema(parentNode);
155
+ const fieldSchema =
156
+ (parentSchema.info as Record<string, ImplicitFieldSchema>)[fieldKey] ??
157
+ fail("Expected field schema");
158
+ if (fieldSchema instanceof FieldSchema && fieldSchema.kind === FieldKind.Optional) {
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
160
+ (parentNode as any)[fieldKey] = undefined;
161
+ } else {
162
+ throw new UsageError(
163
+ `${fieldKey} is required, and cannot be removed. Please use modify edit instead.`,
164
+ );
165
+ }
166
+ }
167
+ } else if (isRange(source)) {
168
+ const { array, startIndex, endIndex } = getRangeInfo(source, idGenerator);
169
+ array.removeRange(startIndex, endIndex);
170
+ }
171
+ return treeEdit;
172
+ }
173
+ case "modify": {
174
+ const node = getNodeFromTarget(treeEdit.target, idGenerator);
175
+ const { treeNodeSchema } = getSimpleNodeSchema(node);
176
+
177
+ const fieldSchema =
178
+ (treeNodeSchema.info as Record<string, ImplicitFieldSchema>)[treeEdit.field] ??
179
+ fail("Expected field schema");
180
+
181
+ const modification = treeEdit.modification;
182
+
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
184
+ const schemaIdentifier = (modification as any)[typeField];
185
+
186
+ let insertedObject: TreeNode | undefined;
187
+ // if fieldSchema is a LeafnodeSchema, we can check that it's a valid type and set the field.
188
+ if (isPrimitive(modification)) {
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
190
+ (node as any)[treeEdit.field] = modification;
191
+ }
192
+ // If the fieldSchema is a function we can grab the constructor and make an instance of that node.
193
+ else if (typeof fieldSchema === "function") {
194
+ const simpleSchema = fieldSchema as unknown as new (dummy: unknown) => TreeNode;
195
+ populateDefaults(modification, definitionMap);
196
+ const constructedModification = new simpleSchema(modification);
197
+ validator?.(constructedModification);
198
+ insertedObject = constructedModification;
199
+
200
+ if (Array.isArray(modification)) {
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
202
+ const field = (node as any)[treeEdit.field] as TreeArrayNode;
203
+ assert(Array.isArray(field), "the field must be an array node");
204
+ assert(
205
+ Array.isArray(constructedModification),
206
+ "the modification must be an array node",
207
+ );
208
+ field.removeRange(0);
209
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
210
+ (node as any)[treeEdit.field] = constructedModification;
211
+ } else {
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
213
+ (node as any)[treeEdit.field] = constructedModification;
214
+ }
215
+ }
216
+ // If the fieldSchema is of type FieldSchema, we can check its allowed types and set the field.
217
+ else if (fieldSchema instanceof FieldSchema) {
218
+ if (fieldSchema.kind === FieldKind.Optional && modification === undefined) {
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
220
+ (node as any)[treeEdit.field] = undefined;
221
+ } else {
222
+ for (const allowedType of fieldSchema.allowedTypeSet.values()) {
223
+ if (allowedType.identifier === schemaIdentifier) {
224
+ if (typeof allowedType === "function") {
225
+ const simpleSchema = allowedType as unknown as new (
226
+ dummy: unknown,
227
+ ) => TreeNode;
228
+ const constructedObject = new simpleSchema(modification);
229
+ insertedObject = constructedObject;
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
231
+ (node as any)[treeEdit.field] = constructedObject;
232
+ } else {
233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
234
+ (node as any)[treeEdit.field] = modification;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return insertedObject === undefined
241
+ ? treeEdit
242
+ : {
243
+ ...treeEdit,
244
+ modification: contentWithIds(insertedObject, idGenerator),
245
+ };
246
+ }
247
+ case "move": {
248
+ // TODO: need to add schema check for valid moves
249
+ const source = treeEdit.source;
250
+ const destination = treeEdit.destination;
251
+ const { array: destinationArrayNode, index: destinationIndex } = getPlaceInfo(
252
+ destination,
253
+ idGenerator,
254
+ );
255
+
256
+ if (isObjectTarget(source)) {
257
+ const sourceNode = getNodeFromTarget(source, idGenerator);
258
+ const sourceIndex = Tree.key(sourceNode) as number;
259
+ const sourceArrayNode = Tree.parent(sourceNode) as TreeArrayNode;
260
+ const sourceArraySchema = Tree.schema(sourceArrayNode);
261
+ if (sourceArraySchema.kind !== NodeKind.Array) {
262
+ throw new UsageError("the source node must be within an arrayNode");
263
+ }
264
+ const destinationArraySchema = Tree.schema(destinationArrayNode);
265
+ const allowedTypes = [
266
+ ...normalizeAllowedTypes(destinationArraySchema.info as ImplicitAllowedTypes),
267
+ ];
268
+ const nodeToMove = sourceArrayNode.at(sourceIndex);
269
+ assert(nodeToMove !== undefined, "node to move must exist");
270
+ if (isNodeAllowedType(nodeToMove as TreeNode, allowedTypes)) {
271
+ destinationArrayNode.moveRangeToIndex(
272
+ destinationIndex,
273
+ sourceIndex,
274
+ sourceIndex + 1,
275
+ sourceArrayNode,
276
+ );
277
+ } else {
278
+ throw new UsageError("Illegal node type in destination array");
279
+ }
280
+ } else if (isRange(source)) {
281
+ const {
282
+ array,
283
+ startIndex: sourceStartIndex,
284
+ endIndex: sourceEndIndex,
285
+ } = getRangeInfo(source, idGenerator);
286
+ const destinationArraySchema = Tree.schema(destinationArrayNode);
287
+ const allowedTypes = [
288
+ ...normalizeAllowedTypes(destinationArraySchema.info as ImplicitAllowedTypes),
289
+ ];
290
+ for (let i = sourceStartIndex; i < sourceEndIndex; i++) {
291
+ const nodeToMove = array.at(i);
292
+ assert(nodeToMove !== undefined, "node to move must exist");
293
+ if (!isNodeAllowedType(nodeToMove as TreeNode, allowedTypes)) {
294
+ throw new UsageError("Illegal node type in destination array");
295
+ }
296
+ }
297
+ destinationArrayNode.moveRangeToIndex(
298
+ destinationIndex,
299
+ sourceStartIndex,
300
+ sourceEndIndex,
301
+ array,
302
+ );
303
+ }
304
+ return treeEdit;
305
+ }
306
+ default: {
307
+ fail("invalid tree edit");
308
+ }
309
+ }
310
+ }
311
+
312
+ function isNodeAllowedType(node: TreeNode, allowedTypes: TreeNodeSchema[]): boolean {
313
+ for (const allowedType of allowedTypes) {
314
+ if (Tree.is(node, allowedType)) {
315
+ return true;
316
+ }
317
+ }
318
+ return false;
319
+ }
320
+
321
+ function isPrimitive(content: unknown): boolean {
322
+ return (
323
+ typeof content === "number" ||
324
+ typeof content === "string" ||
325
+ typeof content === "boolean" ||
326
+ content === undefined ||
327
+ content === null
328
+ );
329
+ }
330
+
331
+ function isObjectTarget(selection: Selection): selection is ObjectTarget {
332
+ return Object.keys(selection).length === 1 && "target" in selection;
333
+ }
334
+
335
+ function isRange(selection: Selection): selection is Range {
336
+ return "from" in selection && "to" in selection;
337
+ }
338
+
339
+ interface RangeInfo {
340
+ array: TreeArrayNode;
341
+ startIndex: number;
342
+ endIndex: number;
343
+ }
344
+
345
+ function getRangeInfo(range: Range, idGenerator: IdGenerator): RangeInfo {
346
+ const { array: arrayFrom, index: startIndex } = getPlaceInfo(range.from, idGenerator);
347
+ const { array: arrayTo, index: endIndex } = getPlaceInfo(range.to, idGenerator);
348
+
349
+ if (arrayFrom !== arrayTo) {
350
+ throw new UsageError(
351
+ 'The "from" node and "to" nodes of the range must be in the same parent array.',
352
+ );
353
+ }
354
+
355
+ return { array: arrayFrom, startIndex, endIndex };
356
+ }
357
+
358
+ function getPlaceInfo(
359
+ place: ObjectPlace | ArrayPlace,
360
+ idGenerator: IdGenerator,
361
+ ): {
362
+ array: TreeArrayNode;
363
+ index: number;
364
+ } {
365
+ if (place.type === "arrayPlace") {
366
+ const parent = idGenerator.getNode(place.parentId) ?? fail("Expected parent node");
367
+ const child = (parent as unknown as Record<string, unknown>)[place.field];
368
+ if (child === undefined) {
369
+ throw new UsageError(`No child under field field`);
370
+ }
371
+ const schema = Tree.schema(child as TreeNode);
372
+ if (schema.kind !== NodeKind.Array) {
373
+ throw new UsageError("Expected child to be in an array node");
374
+ }
375
+ return {
376
+ array: child as TreeArrayNode,
377
+ index: place.location === "start" ? 0 : (child as TreeArrayNode).length,
378
+ };
379
+ } else {
380
+ const node = getNodeFromTarget(place, idGenerator);
381
+ const nodeIndex = Tree.key(node);
382
+ const parent = Tree.parent(node);
383
+ if (parent === undefined) {
384
+ throw new UsageError("TODO: root node target not supported");
385
+ }
386
+ const schema = Tree.schema(parent);
387
+ if (schema.kind !== NodeKind.Array) {
388
+ throw new UsageError("Expected child to be in an array node");
389
+ }
390
+ return {
391
+ array: parent as unknown as TreeArrayNode,
392
+ index: place.place === "before" ? (nodeIndex as number) : (nodeIndex as number) + 1,
393
+ };
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Returns the target node with the matching internal objectId using the provided {@link ObjectTarget}
399
+ */
400
+ function getNodeFromTarget(target: ObjectTarget, idGenerator: IdGenerator): TreeNode {
401
+ const node = idGenerator.getNode(target.target);
402
+ assert(node !== undefined, "objectId does not exist in nodeMap");
403
+ return node;
404
+ }
405
+
406
+ /**
407
+ * Checks that the objectIds of the Tree Nodes within the givin the {@link TreeEdit} exist within the given {@link IdGenerator}
408
+ *
409
+ * @throws An {@link UsageError} if the objectIdKey does not exist in the {@link IdGenerator}
410
+ */
411
+ function assertObjectIdsExist(treeEdit: TreeEdit, idGenerator: IdGenerator): void {
412
+ switch (treeEdit.type) {
413
+ case "insert": {
414
+ if (treeEdit.destination.type === "objectPlace") {
415
+ if (idGenerator.getNode(treeEdit.destination.target) === undefined) {
416
+ throw new UsageError(`objectIdKey ${treeEdit.destination.target} does not exist`);
417
+ }
418
+ } else {
419
+ if (idGenerator.getNode(treeEdit.destination.parentId) === undefined) {
420
+ throw new UsageError(`objectIdKey ${treeEdit.destination.parentId} does not exist`);
421
+ }
422
+ }
423
+ break;
424
+ }
425
+ case "remove": {
426
+ if (isRange(treeEdit.source)) {
427
+ const missingObjectIds = [
428
+ treeEdit.source.from.target,
429
+ treeEdit.source.to.target,
430
+ ].filter((id) => !idGenerator.getNode(id));
431
+
432
+ if (missingObjectIds.length > 0) {
433
+ throw new UsageError(`objectIdKeys [${missingObjectIds}] does not exist`);
434
+ }
435
+ } else if (
436
+ isObjectTarget(treeEdit.source) &&
437
+ idGenerator.getNode(treeEdit.source.target) === undefined
438
+ ) {
439
+ throw new UsageError(`objectIdKey ${treeEdit.source.target} does not exist`);
440
+ }
441
+ break;
442
+ }
443
+ case "modify": {
444
+ if (idGenerator.getNode(treeEdit.target.target) === undefined) {
445
+ throw new UsageError(`objectIdKey ${treeEdit.target.target} does not exist`);
446
+ }
447
+ break;
448
+ }
449
+ case "move": {
450
+ const invalidObjectIds: string[] = [];
451
+ // check the source
452
+ if (isRange(treeEdit.source)) {
453
+ const missingObjectIds = [
454
+ treeEdit.source.from.target,
455
+ treeEdit.source.to.target,
456
+ ].filter((id) => !idGenerator.getNode(id));
457
+
458
+ if (missingObjectIds.length > 0) {
459
+ invalidObjectIds.push(...missingObjectIds);
460
+ }
461
+ } else if (
462
+ isObjectTarget(treeEdit.source) &&
463
+ idGenerator.getNode(treeEdit.source.target) === undefined
464
+ ) {
465
+ invalidObjectIds.push(treeEdit.source.target);
466
+ }
467
+
468
+ // check the destination
469
+ if (treeEdit.destination.type === "objectPlace") {
470
+ if (idGenerator.getNode(treeEdit.destination.target) === undefined) {
471
+ invalidObjectIds.push(treeEdit.destination.target);
472
+ }
473
+ } else {
474
+ if (idGenerator.getNode(treeEdit.destination.parentId) === undefined) {
475
+ invalidObjectIds.push(treeEdit.destination.parentId);
476
+ }
477
+ }
478
+ if (invalidObjectIds.length > 0) {
479
+ throw new UsageError(`objectIdKeys [${invalidObjectIds}] does not exist`);
480
+ }
481
+ break;
482
+ }
483
+ default: {
484
+ break;
485
+ }
486
+ }
487
+ }
488
+
489
+ interface SchemaInfo {
490
+ treeNodeSchema: TreeNodeSchema;
491
+ simpleNodeSchema: new (dummy: unknown) => TreeNode;
492
+ }
493
+
494
+ function getSimpleNodeSchema(node: TreeNode): SchemaInfo {
495
+ const treeNodeSchema = Tree.schema(node);
496
+ const simpleNodeSchema = treeNodeSchema as unknown as new (dummy: unknown) => TreeNode;
497
+ return { treeNodeSchema, simpleNodeSchema };
498
+ }
@@ -0,0 +1,177 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import type { JsonPrimitive } from "./jsonTypes.js";
7
+
8
+ /**
9
+ * TODO: The current scheme does not allow manipulation of arrays of primitive values because you cannot refer to them.
10
+ * We could accomplish this via a path (probably JSON Pointer or JSONPath) from a possibly-null objectId, or wrap arrays in an identified object.
11
+ *
12
+ * TODO: only 100 object fields total are allowed by OpenAI right now, so larger schemas will fail faster if we have a bunch of schema types generated for type-specific edits.
13
+ *
14
+ * TODO: experiment using https://github.com/outlines-dev/outlines (and maybe a llama model) to avoid many of the annoyances of OpenAI's JSON Schema subset.
15
+ *
16
+ * TODO: without field count limits, we could generate a schema for valid paths from the root object to any field, but it's not clear how useful that would be.
17
+ *
18
+ * TODO: We don't supported nested arrays yet.
19
+ *
20
+ * TODO: Add a prompt suggestion API!
21
+ *
22
+ * TODO: Could encourage the model to output more technical explanations of the edits (e.g. "insert a new Foo after "Foo2").
23
+ *
24
+ * TODO: Get explanation strings from o1.
25
+ *
26
+ * TODO: Tests of range edits.
27
+ *
28
+ * TODO: Handle 429 rate limit error from OpenAI.
29
+ *
30
+ * TODO: Add an app-specific guidance string.
31
+ *
32
+ * TODO: Give the model a final chance to evaluate the result.
33
+ *
34
+ * TODO: Separate system prompt into [system, user, system] for security.
35
+ *
36
+ * TODO: Top level arrays are not supported with current DSL.
37
+ *
38
+ * TODO: Structured Output fails when multiple schema types have the same first field name (e.g. id: sf.identifier on multiple types).
39
+ *
40
+ * TODO: Pass descriptions from schema metadata to the generated TS types that we put in the prompt
41
+ */
42
+
43
+ /**
44
+ * This is the field we force the LLM to generate to avoid any type ambiguity (e.g. a vector and a point both have x/y and are ambiguous without the LLM telling us which it means).
45
+ */
46
+ export const typeField = "__fluid_type";
47
+
48
+ /**
49
+ * A field that is auto-generated and injected into nodes before passing data to the LLM to ensure the LLM can refer to nodes in a stable way.
50
+ */
51
+ export const objectIdKey = "__fluid_objectId";
52
+
53
+ /**
54
+ * Describes an edit to a field within a node.
55
+ * @remarks TODO: what is the [key: string] for?
56
+ */
57
+ export interface TreeEditObject {
58
+ [key: string]: TreeEditValue;
59
+ [typeField]: string;
60
+ }
61
+ /**
62
+ * An array of {@link TreeEditValue}'s, allowing a single {@link TreeEdit} to contain edits to multiple fields.
63
+ */
64
+ export type TreeEditArray = TreeEditValue[];
65
+
66
+ /**
67
+ * The potential values for a given {@link TreeEdit}.
68
+ * @remarks These values are typically a field within a node or an entire node,
69
+ */
70
+ export type TreeEditValue = JsonPrimitive | TreeEditObject | TreeEditArray;
71
+
72
+ /**
73
+ * This is the the final object we expected from an LLM response.
74
+ * @remarks Because TreeEdit can be multiple different types (polymorphic),
75
+ * we need to wrap to avoid anyOf at the root level when generating the necessary JSON Schema.
76
+ */
77
+ export interface EditWrapper {
78
+ // eslint-disable-next-line @rushstack/no-new-null
79
+ edit: TreeEdit | null;
80
+ }
81
+
82
+ /**
83
+ * Union type representing all possible types of edits that can be made to a tree.
84
+ */
85
+ export type TreeEdit = Insert | Modify | Remove | Move;
86
+
87
+ /**
88
+ * The base interface for all types of {@link TreeEdit}.
89
+ */
90
+ export interface Edit {
91
+ explanation: string;
92
+ type: "insert" | "modify" | "remove" | "move";
93
+ }
94
+
95
+ /**
96
+ * This object provides a way to 'select' either a given node or a range of nodes in an array.
97
+ */
98
+ export type Selection = ObjectTarget | Range;
99
+
100
+ /**
101
+ * A Target object for an {@link TreeEdit}, identified by the target object's Id
102
+ */
103
+ export interface ObjectTarget {
104
+ target: string;
105
+ }
106
+
107
+ /**
108
+ * Desribes where an object can be inserted into an array.
109
+ * For example, if you have an array with 5 objects, and you insert an object at index 3, this differentiates whether you want
110
+ * the existing item at index 3 to be shifted forward (if the 'location' is 'start') or shifted backwards (if the 'location' is 'end')
111
+ *
112
+ * @remarks TODO: Allow support for nested arrays
113
+ */
114
+ export interface ArrayPlace {
115
+ type: "arrayPlace";
116
+ parentId: string;
117
+ field: string;
118
+ location: "start" | "end";
119
+ }
120
+
121
+ /**
122
+ * Desribes where an object can be inserted into an array.
123
+ * For example, if you have an array with 5 objects, and you insert an object at index 3, this differentiates whether you want
124
+ * the existing item at index 3 to be shifted forward (if the 'location' is 'start') or shifted backwards (if the 'location' is 'end')
125
+ *
126
+ * @remarks Why does this and {@link ArrayPlace} exist together?
127
+ */
128
+ export interface ObjectPlace extends ObjectTarget {
129
+ type: "objectPlace";
130
+ // No "start" or "end" because we don't have a way to refer to arrays directly.
131
+ place: "before" | "after";
132
+ }
133
+
134
+ /**
135
+ * A range of objects within an array. This allows the LLM to select multiple nodes at once,
136
+ * for example during an {@link Remove} operation to remove a range of nodes.
137
+ */
138
+ export interface Range {
139
+ from: ObjectPlace;
140
+ to: ObjectPlace;
141
+ }
142
+
143
+ /**
144
+ * Describes an operation to insert a new node into the tree.
145
+ */
146
+ export interface Insert extends Edit {
147
+ type: "insert";
148
+ content: TreeEditObject | JsonPrimitive;
149
+ destination: ObjectPlace | ArrayPlace;
150
+ }
151
+
152
+ /**
153
+ * Describes an operation to modify an existing node in the tree.
154
+ */
155
+ export interface Modify extends Edit {
156
+ type: "modify";
157
+ target: ObjectTarget;
158
+ field: string;
159
+ modification: TreeEditValue;
160
+ }
161
+
162
+ /**
163
+ * Describes an operation to remove either a specific node or a range of nodes in an array.
164
+ */
165
+ export interface Remove extends Edit {
166
+ type: "remove";
167
+ source: Selection;
168
+ }
169
+
170
+ /**
171
+ * Describes an operation to move a node within an array
172
+ */
173
+ export interface Move extends Edit {
174
+ type: "move";
175
+ source: Selection;
176
+ destination: ObjectPlace | ArrayPlace;
177
+ }