Skip to content

Commit 523aa5e

Browse files
authored
Improve schema naming utils in tree-agent package (#25568)
This covers more cases - e.g. recursive inlined types, cleans up usage, and improves docs.
1 parent a8c60db commit 523aa5e

File tree

4 files changed

+346
-108
lines changed

4 files changed

+346
-108
lines changed

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

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@
66
import { UsageError } from "@fluidframework/telemetry-utils/internal";
77
import type {
88
ImplicitFieldSchema,
9-
RestrictiveStringRecord,
109
TreeFieldFromImplicitField,
11-
TreeObjectNode,
10+
TreeNodeSchema,
1211
} from "@fluidframework/tree";
1312
import { TreeNode, NodeKind, Tree } from "@fluidframework/tree";
14-
import { getSimpleSchema } from "@fluidframework/tree/alpha";
1513
import type {
16-
ObjectNodeSchema,
1714
ReadableField,
1815
TreeBranch,
1916
FactoryContentObject,
2017
InsertableContent,
2118
UnsafeUnknownSchema,
2219
ReadSchema,
2320
} from "@fluidframework/tree/alpha";
21+
import { getSimpleSchema, ObjectNodeSchema } from "@fluidframework/tree/alpha";
2422
import { normalizeFieldSchema, type TreeMapNode } from "@fluidframework/tree/internal";
2523
import type { BaseChatModel } from "@langchain/core/language_models/chat_models"; // eslint-disable-line import/no-internal-modules
2624
import { HumanMessage, SystemMessage } from "@langchain/core/messages"; // eslint-disable-line import/no-internal-modules
@@ -34,12 +32,14 @@ import {
3432
constructNode,
3533
fail,
3634
failUsage,
37-
getFriendlySchema,
38-
getFriendlySchemaName,
35+
getFriendlyName,
36+
unqualifySchema,
3937
getZodSchemaAsTypeScript,
4038
llmDefault,
4139
type SchemaDetails,
4240
type TreeView,
41+
findNamedSchemas,
42+
isNamedSchema,
4343
} from "./utils.js";
4444

4545
const functionName = "editTree";
@@ -173,13 +173,10 @@ export class FunctioningSemanticAgent<TRoot extends ImplicitFieldSchema>
173173
);
174174
const tree = this.queryTree;
175175
const create: Record<string, (input: FactoryContentObject) => TreeNode> = {};
176-
visitObjectNodeSchema(tree.schema, (schema) => {
177-
const name =
178-
getFriendlySchemaName(schema.identifier) ??
179-
fail("Expected friendly name for object node schema");
180-
181-
create[name] = (input: FactoryContentObject) => constructObjectNode(schema, input);
182-
});
176+
for (const schema of findNamedSchemas(tree.schema)) {
177+
const name = getFriendlyName(schema);
178+
create[name] = (input: FactoryContentObject) => constructTreeNode(schema, input);
179+
}
183180
if (this.options?.validator?.(functionCode) === false) {
184181
this.options?.log?.(`#### Code Validation Failed\n\n`);
185182
return "Code validation failed";
@@ -347,7 +344,7 @@ export class FunctioningSemanticAgent<TRoot extends ImplicitFieldSchema>
347344
break;
348345
}
349346
case NodeKind.Object: {
350-
exampleObjectName ??= getFriendlySchemaName(definition);
347+
exampleObjectName ??= unqualifySchema(definition);
351348
break;
352349
}
353350
// No default
@@ -356,15 +353,10 @@ export class FunctioningSemanticAgent<TRoot extends ImplicitFieldSchema>
356353

357354
const { domainTypes } = generateEditTypesForPrompt(schema, simpleSchema);
358355
for (const [key, value] of Object.entries(domainTypes)) {
359-
const friendlyKey = getFriendlySchemaName(key);
360356
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
361357
delete domainTypes[key];
362-
if (
363-
friendlyKey !== undefined &&
364-
friendlyKey !== "string" &&
365-
friendlyKey !== "number" &&
366-
friendlyKey !== "boolean"
367-
) {
358+
if (isNamedSchema(key)) {
359+
const friendlyKey = unqualifySchema(key);
368360
domainTypes[friendlyKey] = value;
369361
}
370362
}
@@ -429,7 +421,7 @@ ${getTreeMapNodeDocumentation(mapInterfaceName)}
429421
430422
`;
431423

432-
const rootTypes = [...simpleSchema.root.allowedTypesIdentifiers];
424+
const rootTypes = normalizeFieldSchema(schema).allowedTypeSet;
433425
const prompt = `You are a helpful assistant collaborating with the user on a document. The document state is a JSON tree, and you are able to analyze and edit it.
434426
The JSON tree adheres to the following Typescript schema:
435427
@@ -451,7 +443,7 @@ It may be synchronous or asynchronous.
451443
The ${functionName} function must have a first parameter which has a \`root\` property.
452444
This \`root\` property holds the current state of the tree as shown above.
453445
You may mutate any part of the tree as necessary, taking into account the caveats around arrays and maps detailed below.
454-
You may also set the \`root\` property to be an entirely new value as long as it is one of the types allowed at the root of the tree (\`${rootTypes.map((t) => getFriendlySchemaName(t)).join(" | ")}\`).
446+
You may also set the \`root\` property to be an entirely new value as long as it is one of the types allowed at the root of the tree (\`${Array.from(rootTypes.values(), (t) => getFriendlyName(t)).join(" | ")}\`).
455447
${helperMethodExplanation}
456448
457449
${hasArrays ? arrayEditing : ""}${hasMaps ? mapEditing : ""}### Additional Notes
@@ -465,7 +457,7 @@ ${builderExplanation}Finally, double check that the edits would accomplish the u
465457
### Application data
466458
467459
${domainHints}
468-
The current state of the application tree (a \`${getFriendlySchema(field)}\`) is:
460+
The current state of the application tree (a \`${field === undefined ? "undefined" : getFriendlyName(Tree.schema(field))}\`) is:
469461
470462
\`\`\`JSON
471463
${stringified}
@@ -731,19 +723,6 @@ function uncapitalize(str: string): string {
731723
return str.charAt(0).toLowerCase() + str.slice(1);
732724
}
733725

734-
function visitObjectNodeSchema(
735-
schema: ImplicitFieldSchema,
736-
visitor: (schema: ObjectNodeSchema) => void,
737-
): void {
738-
const normalizedSchema = normalizeFieldSchema(schema);
739-
for (const nodeSchema of normalizedSchema.allowedTypeSet) {
740-
if (nodeSchema.kind === NodeKind.Object) {
741-
visitor(nodeSchema as ObjectNodeSchema);
742-
}
743-
visitObjectNodeSchema([...nodeSchema.childTypes], visitor);
744-
}
745-
}
746-
747726
function processLlmCode(code: string): string {
748727
// TODO: use a library like Acorn to analyze the code more robustly
749728
const regex = new RegExp(`function\\s+${functionName}\\s*\\(`);
@@ -755,33 +734,32 @@ function processLlmCode(code: string): string {
755734
}
756735

757736
/**
758-
* Creates an unhydrated object node and populates it with `llmDefault` values if they exist.
737+
* Creates an unhydrated node of the given schema with the given value.
738+
* @remarks If the schema is an object with {@link llmDefault | default values}, this function populates the node with those defaults.
759739
*/
760-
function constructObjectNode(
761-
schema: ObjectNodeSchema,
762-
input: FactoryContentObject,
763-
): TreeObjectNode<RestrictiveStringRecord<ImplicitFieldSchema>> {
764-
const inputWithDefaults: Record<string, InsertableContent | undefined> = {};
765-
for (const [key, field] of schema.fields) {
766-
if (input[key] === undefined) {
767-
if (
768-
typeof field.metadata.custom === "object" &&
769-
field.metadata.custom !== null &&
770-
llmDefault in field.metadata.custom
771-
) {
772-
const defaulter = field.metadata.custom[llmDefault];
773-
if (typeof defaulter === "function") {
774-
const defaultValue: unknown = defaulter();
775-
if (defaultValue !== undefined) {
776-
inputWithDefaults[key] = defaultValue;
740+
function constructTreeNode(schema: TreeNodeSchema, value: FactoryContentObject): TreeNode {
741+
if (schema instanceof ObjectNodeSchema) {
742+
const inputWithDefaults: Record<string, InsertableContent | undefined> = {};
743+
for (const [key, field] of schema.fields) {
744+
if (value[key] === undefined) {
745+
if (
746+
typeof field.metadata.custom === "object" &&
747+
field.metadata.custom !== null &&
748+
llmDefault in field.metadata.custom
749+
) {
750+
const defaulter = field.metadata.custom[llmDefault];
751+
if (typeof defaulter === "function") {
752+
const defaultValue: unknown = defaulter();
753+
if (defaultValue !== undefined) {
754+
inputWithDefaults[key] = defaultValue;
755+
}
777756
}
778757
}
758+
} else {
759+
inputWithDefaults[key] = value[key];
779760
}
780-
} else {
781-
inputWithDefaults[key] = input[key];
782761
}
762+
return constructNode(schema, inputWithDefaults);
783763
}
784-
return constructNode(schema, inputWithDefaults) as TreeObjectNode<
785-
RestrictiveStringRecord<ImplicitFieldSchema>
786-
>;
764+
return constructNode(schema, value);
787765
}

0 commit comments

Comments
 (0)