Skip to content

Commit b5f65c6

Browse files
noenckeCopilot
andauthored
Tag insertable content in tree-agent (#25753)
tree-agent now uses the `tagSchemaContent` API to create insertable nodes rather than creating unhydrated nodes. This means that an LLM can safely do something like the following: ```ts const color = context.create.Color({ r: 255, g: 255, b: 255 }); view.root.background = context.create.Gradient({ start: color, end: color }); ``` which would have previously resulted in an error because `color` is being inserted twice in the tree. Now, the color data is instead _copied_ into the tree, without losing typing information in cases of ambiguity. This means that the LLM can no longer benefit from SharedTree's "hydration" feature - that is, it must do "read-backs" if it wants to obtain a reference to the node that it just inserted into the tree. We have observed that it does not take advantage of this feature, and does frequently commit the error above, so this should improve reliability. --------- Co-authored-by: Copilot <[email protected]>
1 parent 88860f3 commit b5f65c6

File tree

2 files changed

+38
-9
lines changed

2 files changed

+38
-9
lines changed

packages/framework/tree-agent/src/agent.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
InsertableContent,
1616
ReadSchema,
1717
} from "@fluidframework/tree/alpha";
18-
import { ObjectNodeSchema, Tree } from "@fluidframework/tree/alpha";
18+
import { ObjectNodeSchema, Tree, TreeAlpha } from "@fluidframework/tree/alpha";
1919

2020
import type {
2121
SharedTreeChatModel,
@@ -29,7 +29,6 @@ import type {
2929
import { getPrompt, stringifyTree } from "./prompt.js";
3030
import { Subtree } from "./subtree.js";
3131
import {
32-
constructNode,
3332
llmDefault,
3433
type TreeView,
3534
findSchemas,
@@ -174,11 +173,12 @@ export class SharedTreeSemanticAgent<TSchema extends ImplicitFieldSchema> {
174173
* Creates an unhydrated node of the given schema with the given value.
175174
* @remarks If the schema is an object with {@link llmDefault | default values}, this function populates the node with those defaults.
176175
*/
177-
function constructTreeNode(schema: TreeNodeSchema, value: FactoryContentObject): TreeNode {
176+
function constructTreeNode(schema: TreeNodeSchema, content: FactoryContentObject): TreeNode {
177+
let toInsert = content;
178178
if (schema instanceof ObjectNodeSchema) {
179-
const inputWithDefaults: Record<string, InsertableContent | undefined> = {};
179+
const contentWithDefaults: Record<string, InsertableContent | undefined> = {};
180180
for (const [key, field] of schema.fields) {
181-
if (value[key] === undefined) {
181+
if (content[key] === undefined) {
182182
if (
183183
typeof field.metadata.custom === "object" &&
184184
field.metadata.custom !== null &&
@@ -188,17 +188,19 @@ function constructTreeNode(schema: TreeNodeSchema, value: FactoryContentObject):
188188
if (typeof defaulter === "function") {
189189
const defaultValue: unknown = defaulter();
190190
if (defaultValue !== undefined) {
191-
inputWithDefaults[key] = defaultValue;
191+
contentWithDefaults[key] = defaultValue;
192192
}
193193
}
194194
}
195195
} else {
196-
inputWithDefaults[key] = value[key];
196+
contentWithDefaults[key] = content[key];
197197
}
198198
}
199-
return constructNode(schema, inputWithDefaults);
199+
toInsert = contentWithDefaults;
200200
}
201-
return constructNode(schema, value);
201+
202+
// Cast to never because tagContentSchema is typed to only accept InsertableContent, but we know that 'toInsert' (either the original content or contentWithDefaults) produces valid content for the schema.
203+
return TreeAlpha.tagContentSchema(schema, toInsert as never);
202204
}
203205

204206
/**

packages/framework/tree-agent/src/test/agent.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,33 @@ describe("Semantic Agent", () => {
272272
);
273273
});
274274

275+
it("can insert content multiple times", async () => {
276+
class Color extends sf.object("Color", {
277+
r: sf.number,
278+
g: sf.number,
279+
b: sf.number,
280+
}) {}
281+
class Gradient extends sf.object("Gradient", {
282+
startColor: Color,
283+
endColor: Color,
284+
}) {}
285+
const view = independentView(new TreeViewConfiguration({ schema: Gradient }), {});
286+
view.initialize({ startColor: { r: 0, g: 0, b: 0 }, endColor: { r: 0, g: 0, b: 0 } });
287+
const code = `const white = context.create.Color({ r: 255, g: 255, b: 255 });
288+
context.root = context.create.Gradient({ startColor: white, endColor: white });`;
289+
const model: SharedTreeChatModel = {
290+
editToolName,
291+
async query({ edit }) {
292+
const result = await edit(code);
293+
assert(result.type === "success", result.message);
294+
return result.message;
295+
},
296+
};
297+
const agent = new SharedTreeSemanticAgent(model, view);
298+
await agent.query("Query");
299+
assert.equal(view.root.startColor.r, 255);
300+
});
301+
275302
describe("context helpers", () => {
276303
it("passes constructors to edit code", async () => {
277304
const sfLocal = new SchemaFactory("Test");

0 commit comments

Comments
 (0)