@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,294 @@
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 {
8
+ NodeKind,
9
+ type ImplicitFieldSchema,
10
+ type TreeFieldFromImplicitField,
11
+ getJsonSchema,
12
+ type JsonFieldSchema,
13
+ type JsonNodeSchema,
14
+ type JsonSchemaRef,
15
+ type JsonTreeSchema,
16
+ getSimpleSchema,
17
+ Tree,
18
+ type TreeNode,
19
+ } from "@fluidframework/tree/internal";
20
+ // eslint-disable-next-line import/no-internal-modules
21
+ import { createZodJsonValidator } from "typechat/zod";
22
+
23
+ import { objectIdKey, type TreeEdit } from "./agentEditTypes.js";
24
+ import type { IdGenerator } from "./idGenerator.js";
25
+ import { generateGenericEditTypes } from "./typeGeneration.js";
26
+ import { fail } from "./utils.js";
27
+
28
+ /**
29
+ * A log of edits that have been made to a tree.
30
+ * @remarks This is primarily used to help an LLM keep track of the active changes it has made.
31
+ */
32
+ export type EditLog = {
33
+ edit: TreeEdit;
34
+ error?: string;
35
+ }[];
36
+
37
+ /**
38
+ * TBD
39
+ */
40
+ export function toDecoratedJson(
41
+ idGenerator: IdGenerator,
42
+ root: TreeFieldFromImplicitField<ImplicitFieldSchema>,
43
+ ): string {
44
+ idGenerator.assignIds(root);
45
+ const stringified: string = JSON.stringify(root, (_, value) => {
46
+ if (typeof value === "object" && !Array.isArray(value) && value !== null) {
47
+ // TODO: SharedTree Team needs to either publish TreeNode as a class to use .instanceof() or a typeguard.
48
+ // Uncomment this assertion back once we have a typeguard ready.
49
+ // assert(isTreeNode(node), "Non-TreeNode value in tree.");
50
+ const objId =
51
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
52
+ idGenerator.getId(value) ?? fail("ID of new node should have been assigned.");
53
+ assert(
54
+ !Object.prototype.hasOwnProperty.call(value, objectIdKey),
55
+ `Collision of object id property.`,
56
+ );
57
+ return {
58
+ [objectIdKey]: objId,
59
+ ...value,
60
+ } as unknown;
61
+ }
62
+ return value as unknown;
63
+ });
64
+ return stringified;
65
+ }
66
+
67
+ /**
68
+ * Generates a prompt designed to make an LLM produce a plan to edit the SharedTree to accomplish a user-specified goal.
69
+ */
70
+ export function getPlanningSystemPrompt(
71
+ treeNode: TreeNode,
72
+ userPrompt: string,
73
+ systemRoleContext?: string,
74
+ ): string {
75
+ const schema = Tree.schema(treeNode);
76
+
77
+ const promptFriendlySchema = getPromptFriendlyTreeSchema(getJsonSchema(schema));
78
+ const role = `I'm an agent who makes plans for another agent to achieve a user-specified goal to update the state of an application.${
79
+ systemRoleContext === undefined
80
+ ? ""
81
+ : `
82
+ The other agent follows this guidance: ${systemRoleContext}`
83
+ }`;
84
+
85
+ const systemPrompt = `
86
+ ${role}
87
+ The application state tree is a JSON object with the following schema: ${promptFriendlySchema}
88
+ The current state is: ${JSON.stringify(treeNode)}.
89
+ The user requested that I accomplish the following goal:
90
+ "${userPrompt}"
91
+ I've made a plan to accomplish this goal by doing a sequence of edits to the tree.
92
+ Edits can include setting the root, inserting, modifying, removing, or moving elements in the tree.
93
+ Here is my plan:`;
94
+
95
+ return systemPrompt;
96
+ }
97
+
98
+ /**
99
+ * Generates a prompt that provides a history of the edits an LLM has made to a SharedTree as well as any errors that occured from attemping to apply each respsecitve edit to the tree.
100
+ */
101
+ export function createEditListHistoryPrompt(edits: EditLog): string {
102
+ return edits
103
+ .map((edit, index) => {
104
+ const error =
105
+ edit.error === undefined
106
+ ? ""
107
+ : ` This edit produced an error, and was discarded. The error message was: "${edit.error}"`;
108
+ return `${index + 1}. ${JSON.stringify(edit.edit)}${error}`;
109
+ })
110
+ .join("\n");
111
+ }
112
+
113
+ /**
114
+ * Generates the main prompt of this explicit strategy.
115
+ * This prompt is designed to give an LLM instructions on how it can modify a SharedTree using specific types of {@link TreeEdit}'s
116
+ * and provides with both a serialized version of the current state of the provided tree node as well as the interfaces that compromise said tree nodes data.
117
+ */
118
+ export function getEditingSystemPrompt(
119
+ userPrompt: string,
120
+ idGenerator: IdGenerator,
121
+ treeNode: TreeNode,
122
+ log: EditLog,
123
+ appGuidance?: string,
124
+ plan?: string,
125
+ ): string {
126
+ const schema = Tree.schema(treeNode);
127
+ const promptFriendlySchema = getPromptFriendlyTreeSchema(getJsonSchema(schema));
128
+ const decoratedTreeJson = toDecoratedJson(idGenerator, treeNode);
129
+
130
+ const role = `You are a collaborative agent who interacts with a JSON tree by performing edits to achieve a user-specified goal.${
131
+ appGuidance === undefined
132
+ ? ""
133
+ : `
134
+ The application that owns the JSON tree has the following guidance about your role: ${appGuidance}`
135
+ }`;
136
+
137
+ const treeSchemaString = createZodJsonValidator(
138
+ ...generateGenericEditTypes(getSimpleSchema(schema), false),
139
+ ).getSchemaText();
140
+
141
+ // TODO: security: user prompt in system prompt
142
+ const systemPrompt = `
143
+ ${role}
144
+ Edits are JSON objects that conform to the following schema.
145
+ The top level object you produce is an "EditWrapper" object which contains one of "Insert", "Modify", "Remove", "Move", or null.
146
+ ${treeSchemaString}
147
+ The tree is a JSON object with the following schema: ${promptFriendlySchema}
148
+ ${plan === undefined ? "" : `You have made a plan to accomplish the user's goal. The plan is: "${plan}". You will perform one or more edits that correspond to that plan to accomplish the goal.`}
149
+ ${
150
+ log.length === 0
151
+ ? ""
152
+ : `You have already performed the following edits:
153
+ ${createEditListHistoryPrompt(log)}
154
+ This means that the current state of the tree reflects these changes.`
155
+ }
156
+ The current state of the tree is: ${decoratedTreeJson}.
157
+ ${log.length > 0 ? "Before you made the above edits t" : "T"}he user requested you accomplish the following goal:
158
+ "${userPrompt}"
159
+ If the goal is now completed or is impossible, you should return null.
160
+ Otherwise, you should create an edit that makes progress towards the goal. It should have an English description ("explanation") of which edit to perform (specifying one of the allowed edit types).`;
161
+ return systemPrompt;
162
+ }
163
+
164
+ /**
165
+ * Generates a prompt designed to make an LLM review the edits it created and applied to a SharedTree based
166
+ * on a user-specified goal. This prompt is designed to give the LLM's ability to correct for mistakes and improve the accuracy/fidelity of its final set of tree edits
167
+ */
168
+ export function getReviewSystemPrompt(
169
+ userPrompt: string,
170
+ idGenerator: IdGenerator,
171
+ treeNode: TreeNode,
172
+ originalDecoratedJson: string,
173
+ appGuidance?: string,
174
+ ): string {
175
+ const schema = Tree.schema(treeNode);
176
+ const promptFriendlySchema = getPromptFriendlyTreeSchema(getJsonSchema(schema));
177
+ const decoratedTreeJson = toDecoratedJson(idGenerator, treeNode);
178
+
179
+ const role = `You are a collaborative agent who interacts with a JSON tree by performing edits to achieve a user-specified goal.${
180
+ appGuidance === undefined
181
+ ? ""
182
+ : `
183
+ The application that owns the JSON tree has the following guidance: ${appGuidance}`
184
+ }`;
185
+
186
+ // TODO: security: user prompt in system prompt
187
+ const systemPrompt = `
188
+ ${role}
189
+ You have performed a number of actions already to accomplish a user request.
190
+ You must review the resulting state to determine if the actions you performed successfully accomplished the user's goal.
191
+ The tree is a JSON object with the following schema: ${promptFriendlySchema}
192
+ The state of the tree BEFORE changes was: ${originalDecoratedJson}.
193
+ The state of the tree AFTER changes is: ${decoratedTreeJson}.
194
+ The user requested that the following goal should be accomplished:
195
+ ${userPrompt}
196
+ Was the goal accomplished?`;
197
+ return systemPrompt;
198
+ }
199
+
200
+ /**
201
+ * Converts a fully-qualified SharedTree schema name to a single-word name for use in textual TypeScript-style types.
202
+ *
203
+ * @remarks
204
+ * - TODO: Determine what to do with user-provided names that include periods (e.g. "Foo.Bar").
205
+ * - TODO: Should probably ensure name starts with an uppercase character.
206
+ */
207
+ export function getPromptFriendlyTreeSchema(jsonSchema: JsonTreeSchema): string {
208
+ let stringifiedSchema = "";
209
+ for (const [name, def] of Object.entries(jsonSchema.$defs)) {
210
+ if (def.type !== "object" || def._treeNodeSchemaKind === NodeKind.Map) {
211
+ continue;
212
+ }
213
+
214
+ let stringifiedEntry = `interface ${getFriendlySchemaName(name)} {`;
215
+
216
+ for (const [fieldName, fieldSchema] of Object.entries(def.properties)) {
217
+ let typeString: string;
218
+ if (isJsonSchemaRef(fieldSchema)) {
219
+ const nextFieldName = fieldSchema.$ref;
220
+ const nextDef = getDef(jsonSchema.$defs, nextFieldName);
221
+ typeString = `${getTypeString(jsonSchema.$defs, [nextFieldName, nextDef])}`;
222
+ } else {
223
+ typeString = `${getAnyOfTypeString(jsonSchema.$defs, fieldSchema.anyOf, true)}`;
224
+ }
225
+ if (def.required && !def.required.includes(fieldName)) {
226
+ typeString = `${typeString} | undefined`;
227
+ }
228
+ stringifiedEntry += ` ${fieldName}: ${typeString};`;
229
+ }
230
+
231
+ stringifiedEntry += " }";
232
+
233
+ stringifiedSchema += (stringifiedSchema === "" ? "" : " ") + stringifiedEntry;
234
+ }
235
+ return stringifiedSchema;
236
+ }
237
+
238
+ function getTypeString(
239
+ defs: Record<string, JsonNodeSchema>,
240
+ [name, currentDef]: [string, JsonNodeSchema],
241
+ ): string {
242
+ const { _treeNodeSchemaKind } = currentDef;
243
+ if (_treeNodeSchemaKind === NodeKind.Leaf) {
244
+ return currentDef.type;
245
+ }
246
+ if (_treeNodeSchemaKind === NodeKind.Object) {
247
+ return getFriendlySchemaName(name);
248
+ }
249
+ if (_treeNodeSchemaKind === NodeKind.Array) {
250
+ const items = currentDef.items;
251
+ const innerType = isJsonSchemaRef(items)
252
+ ? getTypeString(defs, [items.$ref, getDef(defs, items.$ref)])
253
+ : getAnyOfTypeString(defs, items.anyOf);
254
+ return `${innerType}[]`;
255
+ }
256
+ fail("Non-object, non-leaf, non-array schema type.");
257
+ }
258
+
259
+ function getAnyOfTypeString(
260
+ defs: Record<string, JsonNodeSchema>,
261
+ refList: JsonSchemaRef[],
262
+ topLevel = false,
263
+ ): string {
264
+ const typeNames: string[] = [];
265
+ for (const ref of refList) {
266
+ typeNames.push(getTypeString(defs, [ref.$ref, getDef(defs, ref.$ref)]));
267
+ }
268
+ const typeString = typeNames.join(" | ");
269
+ return topLevel ? typeString : `(${typeString})`;
270
+ }
271
+
272
+ function isJsonSchemaRef(field: JsonFieldSchema): field is JsonSchemaRef {
273
+ return (field as JsonSchemaRef).$ref !== undefined;
274
+ }
275
+
276
+ function getDef(defs: Record<string, JsonNodeSchema>, ref: string): JsonNodeSchema {
277
+ // strip the "#/$defs/" prefix
278
+ const strippedRef = ref.slice(8);
279
+ const nextDef = defs[strippedRef];
280
+ assert(nextDef !== undefined, "Ref not found.");
281
+ return nextDef;
282
+ }
283
+
284
+ /**
285
+ * TBD
286
+ */
287
+ export function getFriendlySchemaName(schemaName: string): string {
288
+ const matches = schemaName.match(/[^.]+$/);
289
+ if (matches === null) {
290
+ // empty scope
291
+ return schemaName;
292
+ }
293
+ return matches[0];
294
+ }
@@ -0,0 +1,374 @@
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 { FieldKind, NodeKind, ValueSchema } from "@fluidframework/tree/internal";
8
+ import type {
9
+ SimpleFieldSchema,
10
+ SimpleNodeSchema,
11
+ SimpleTreeSchema,
12
+ } from "@fluidframework/tree/internal";
13
+ import { z } from "zod";
14
+
15
+ import { objectIdKey, typeField } from "./agentEditTypes.js";
16
+ import { fail, getOrCreate, mapIterable } from "./utils.js";
17
+
18
+ /**
19
+ * Zod Object type used to represent & validate the ObjectTarget type within a {@link TreeEdit}.
20
+ * @remarks this is used as a component with {@link generateGenericEditTypes} to produce the final zod validation objects.
21
+ */
22
+ const objectTarget = z.object({
23
+ target: z
24
+ .string()
25
+ .describe(
26
+ `The id of the object (as specified by the object's ${objectIdKey} property) that is being referenced`,
27
+ ),
28
+ });
29
+ /**
30
+ * Zod Object type used to represent & validate the ObjectPlace type within a {@link TreeEdit}.
31
+ * @remarks this is used as a component with {@link generateGenericEditTypes} to produce the final zod validation objects.
32
+ */
33
+ const objectPlace = z
34
+ .object({
35
+ type: z.enum(["objectPlace"]),
36
+ target: z
37
+ .string()
38
+ .describe(
39
+ `The id (${objectIdKey}) of the object that the new/moved object should be placed relative to. This must be the id of an object that already existed in the tree content that was originally supplied.`,
40
+ ),
41
+ place: z
42
+ .enum(["before", "after"])
43
+ .describe(
44
+ "Where the new/moved object will be relative to the target object - either just before or just after",
45
+ ),
46
+ })
47
+ .describe(
48
+ "A pointer to a location either just before or just after an object that is in an array",
49
+ );
50
+ /**
51
+ * Zod Object type used to represent & validate the ArrayPlace type within a {@link TreeEdit}.
52
+ * @remarks this is used as a component with {@link generateGenericEditTypes} to produce the final zod validation objects.
53
+ */
54
+ const arrayPlace = z
55
+ .object({
56
+ type: z.enum(["arrayPlace"]),
57
+ parentId: z
58
+ .string()
59
+ .describe(
60
+ `The id (${objectIdKey}) of the parent object of the array. This must be the id of an object that already existed in the tree content that was originally supplied.`,
61
+ ),
62
+ field: z.string().describe("The key of the array to insert into"),
63
+ location: z
64
+ .enum(["start", "end"])
65
+ .describe("Where to insert into the array - either the start or the end"),
66
+ })
67
+ .describe(
68
+ `either the "start" or "end" of an array, as specified by a "parent" ObjectTarget and a "field" name under which the array is stored (useful for prepending or appending)`,
69
+ );
70
+ /**
71
+ * Zod Object type used to represent & validate the Range type within a {@link TreeEdit}.
72
+ * @remarks this is used as a component with {@link generateGenericEditTypes} to produce the final zod validation objects.
73
+ */
74
+ const range = z
75
+ .object({
76
+ from: objectPlace,
77
+ to: objectPlace,
78
+ })
79
+ .describe(
80
+ 'A range of objects in the same array specified by a "from" and "to" Place. The "to" and "from" objects MUST be in the same array.',
81
+ );
82
+ /**
83
+ * Cache used to prevent repeatedly generating the same Zod validation objects for the same {@link SimpleTreeSchema} as generate propts for repeated calls to an LLM
84
+ */
85
+ const cache = new WeakMap<SimpleTreeSchema, ReturnType<typeof generateGenericEditTypes>>();
86
+
87
+ /**
88
+ * Generates a set of ZOD validation objects for the various types of data that can be put into the provided {@link SimpleTreeSchema}
89
+ * and then uses those sets to generate an all-encompassing ZOD object for each type of {@link TreeEdit} that can validate any of the types of data that can be put into the tree.
90
+ *
91
+ * @returns a Record of schema names to Zod validation objects, and the name of the root schema used to encompass all of the other schemas.
92
+ *
93
+ * @remarks The return type of this function is designed to work with Typechat's createZodJsonValidator as well as be used as the JSON schema for OpenAi's structured output response format.
94
+ */
95
+ export function generateGenericEditTypes(
96
+ schema: SimpleTreeSchema,
97
+ generateDomainTypes: boolean,
98
+ ): [Record<string, Zod.ZodTypeAny>, root: string] {
99
+ return getOrCreate(cache, schema, () => {
100
+ const insertSet = new Set<string>();
101
+ const modifyFieldSet = new Set<string>();
102
+ const modifyTypeSet = new Set<string>();
103
+ const typeMap = new Map<string, Zod.ZodTypeAny>();
104
+ for (const name of schema.definitions.keys()) {
105
+ getOrCreateType(
106
+ schema.definitions,
107
+ typeMap,
108
+ insertSet,
109
+ modifyFieldSet,
110
+ modifyTypeSet,
111
+ name,
112
+ );
113
+ }
114
+ function getType(allowedTypes: ReadonlySet<string>): Zod.ZodTypeAny {
115
+ switch (allowedTypes.size) {
116
+ case 0: {
117
+ return z.never();
118
+ }
119
+ case 1: {
120
+ return (
121
+ typeMap.get(tryGetSingleton(allowedTypes) ?? fail("Expected singleton")) ??
122
+ fail("Unknown type")
123
+ );
124
+ }
125
+ default: {
126
+ const types = Array.from(
127
+ allowedTypes,
128
+ (name) => typeMap.get(name) ?? fail("Unknown type"),
129
+ );
130
+ assert(hasAtLeastTwo(types), "Expected at least two types");
131
+ return z.union(types);
132
+ }
133
+ }
134
+ }
135
+ const insert = z
136
+ .object({
137
+ type: z.literal("insert"),
138
+ explanation: z.string().describe(editDescription),
139
+ content: generateDomainTypes
140
+ ? getType(insertSet)
141
+ : z.any().describe("Domain-specific content here"),
142
+ destination: z.union([arrayPlace, objectPlace]),
143
+ })
144
+ .describe("Inserts a new object at a specific Place or ArrayPlace.");
145
+ const remove = z
146
+ .object({
147
+ type: z.literal("remove"),
148
+ explanation: z.string().describe(editDescription),
149
+ source: z.union([objectTarget, range]),
150
+ })
151
+ .describe("Deletes an object or Range of objects from the tree.");
152
+ const move = z
153
+ .object({
154
+ type: z.literal("move"),
155
+ explanation: z.string().describe(editDescription),
156
+ source: z.union([objectTarget, range]),
157
+ destination: z.union([arrayPlace, objectPlace]),
158
+ })
159
+ .describe("Moves an object or Range of objects to a new Place or ArrayPlace.");
160
+ const modify = z
161
+ .object({
162
+ type: z.enum(["modify"]),
163
+ explanation: z.string().describe(editDescription),
164
+ target: objectTarget,
165
+ field: z.enum([...modifyFieldSet] as [string, ...string[]]), // Modify with appropriate fields
166
+ modification: generateDomainTypes
167
+ ? getType(modifyTypeSet)
168
+ : z.any().describe("Domain-specific content here"),
169
+ })
170
+ .describe("Sets a field on a specific ObjectTarget.");
171
+ const editTypes = [insert, remove, move, modify, z.null()] as const;
172
+ const editWrapper = z.object({
173
+ edit: z
174
+ .union(editTypes)
175
+ .describe("The next edit to apply to the tree, or null if the task is complete."),
176
+ });
177
+ const typeRecord: Record<string, Zod.ZodTypeAny> = {
178
+ ObjectTarget: objectTarget,
179
+ ObjectPlace: objectPlace,
180
+ ArrayPlace: arrayPlace,
181
+ Range: range,
182
+ Insert: insert,
183
+ Remove: remove,
184
+ Move: move,
185
+ Modify: modify,
186
+ EditWrapper: editWrapper,
187
+ };
188
+ return [typeRecord, "EditWrapper"];
189
+ });
190
+ }
191
+ const editDescription =
192
+ "A description of what this edit is meant to accomplish in human readable English";
193
+ function getOrCreateType(
194
+ definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
195
+ typeMap: Map<string, Zod.ZodTypeAny>,
196
+ insertSet: Set<string>,
197
+ modifyFieldSet: Set<string>,
198
+ modifyTypeSet: Set<string>,
199
+ definition: string,
200
+ ): Zod.ZodTypeAny {
201
+ return getOrCreate(typeMap, definition, () => {
202
+ const nodeSchema = definitionMap.get(definition) ?? fail("Unexpected definition");
203
+ switch (nodeSchema.kind) {
204
+ case NodeKind.Object: {
205
+ for (const [key, field] of Object.entries(nodeSchema.fields)) {
206
+ // TODO: Remove when AI better
207
+ if (
208
+ Array.from(
209
+ field.allowedTypes,
210
+ (n) => definitionMap.get(n) ?? fail("Unknown definition"),
211
+ ).some((n) => n.kind === NodeKind.Array)
212
+ ) {
213
+ continue;
214
+ }
215
+ modifyFieldSet.add(key);
216
+ for (const type of field.allowedTypes) {
217
+ modifyTypeSet.add(type);
218
+ }
219
+ }
220
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
221
+ const properties = Object.fromEntries(
222
+ Object.entries(nodeSchema.fields)
223
+ .map(([key, field]) => {
224
+ return [
225
+ key,
226
+ getOrCreateTypeForField(
227
+ definitionMap,
228
+ typeMap,
229
+ insertSet,
230
+ modifyFieldSet,
231
+ modifyTypeSet,
232
+ field,
233
+ ),
234
+ ];
235
+ })
236
+ .filter(([, value]) => value !== undefined),
237
+ );
238
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
239
+ properties[typeField] = z.enum([definition]);
240
+ return z.object(properties);
241
+ }
242
+ case NodeKind.Array: {
243
+ for (const [name] of Array.from(
244
+ nodeSchema.allowedTypes,
245
+ (n): [string, SimpleNodeSchema] => [
246
+ n,
247
+ definitionMap.get(n) ?? fail("Unknown definition"),
248
+ ],
249
+ ).filter(
250
+ ([_, schema]) => schema.kind === NodeKind.Object || schema.kind === NodeKind.Leaf,
251
+ )) {
252
+ insertSet.add(name);
253
+ }
254
+ return z.array(
255
+ getTypeForAllowedTypes(
256
+ definitionMap,
257
+ typeMap,
258
+ insertSet,
259
+ modifyFieldSet,
260
+ modifyTypeSet,
261
+ nodeSchema.allowedTypes,
262
+ ),
263
+ );
264
+ }
265
+ case NodeKind.Leaf: {
266
+ switch (nodeSchema.leafKind) {
267
+ case ValueSchema.Boolean: {
268
+ return z.boolean();
269
+ }
270
+ case ValueSchema.Number: {
271
+ return z.number();
272
+ }
273
+ case ValueSchema.String: {
274
+ return z.string();
275
+ }
276
+ case ValueSchema.Null: {
277
+ return z.null();
278
+ }
279
+ default: {
280
+ throw new Error(`Unsupported leaf kind ${NodeKind[nodeSchema.leafKind]}.`);
281
+ }
282
+ }
283
+ }
284
+ default: {
285
+ throw new Error(`Unsupported node kind ${NodeKind[nodeSchema.kind]}.`);
286
+ }
287
+ }
288
+ });
289
+ }
290
+ function getOrCreateTypeForField(
291
+ definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
292
+ typeMap: Map<string, Zod.ZodTypeAny>,
293
+ insertSet: Set<string>,
294
+ modifyFieldSet: Set<string>,
295
+ modifyTypeSet: Set<string>,
296
+ fieldSchema: SimpleFieldSchema,
297
+ ): Zod.ZodTypeAny | undefined {
298
+ switch (fieldSchema.kind) {
299
+ case FieldKind.Required: {
300
+ return getTypeForAllowedTypes(
301
+ definitionMap,
302
+ typeMap,
303
+ insertSet,
304
+ modifyFieldSet,
305
+ modifyTypeSet,
306
+ fieldSchema.allowedTypes,
307
+ );
308
+ }
309
+ case FieldKind.Optional: {
310
+ return z.union([
311
+ z.null(),
312
+ getTypeForAllowedTypes(
313
+ definitionMap,
314
+ typeMap,
315
+ insertSet,
316
+ modifyFieldSet,
317
+ modifyTypeSet,
318
+ fieldSchema.allowedTypes,
319
+ ),
320
+ ]);
321
+ }
322
+ case FieldKind.Identifier: {
323
+ return undefined;
324
+ }
325
+ default: {
326
+ throw new Error(`Unsupported field kind ${NodeKind[fieldSchema.kind]}.`);
327
+ }
328
+ }
329
+ }
330
+ function getTypeForAllowedTypes(
331
+ definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
332
+ typeMap: Map<string, Zod.ZodTypeAny>,
333
+ insertSet: Set<string>,
334
+ modifyFieldSet: Set<string>,
335
+ modifyTypeSet: Set<string>,
336
+ allowedTypes: ReadonlySet<string>,
337
+ ): Zod.ZodTypeAny {
338
+ const single = tryGetSingleton(allowedTypes);
339
+ if (single === undefined) {
340
+ const types = [
341
+ ...mapIterable(allowedTypes, (name) => {
342
+ return getOrCreateType(
343
+ definitionMap,
344
+ typeMap,
345
+ insertSet,
346
+ modifyFieldSet,
347
+ modifyTypeSet,
348
+ name,
349
+ );
350
+ }),
351
+ ];
352
+ assert(hasAtLeastTwo(types), "Expected at least two types");
353
+ return z.union(types);
354
+ } else {
355
+ return getOrCreateType(
356
+ definitionMap,
357
+ typeMap,
358
+ insertSet,
359
+ modifyFieldSet,
360
+ modifyTypeSet,
361
+ single,
362
+ );
363
+ }
364
+ }
365
+ function tryGetSingleton<T>(set: ReadonlySet<T>): T | undefined {
366
+ if (set.size === 1) {
367
+ for (const item of set) {
368
+ return item;
369
+ }
370
+ }
371
+ }
372
+ function hasAtLeastTwo<T>(array: T[]): array is [T, T, ...T[]] {
373
+ return array.length >= 2;
374
+ }