-
Notifications
You must be signed in to change notification settings - Fork 536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ai collab feedback loop upgrades #23517
base: main
Are you sure you want to change the base?
Conversation
…onexistant field or invalid type for field
… 'modify' edit tests in agentEditReducer.spec.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 5 out of 7 changed files in this pull request and generated no comments.
Files not reviewed (2)
- packages/framework/ai-collab/src/test/explicit-strategy/snapshots/Prompt_Regression_Snapshot_Tests/Editing_System_Prompt_With_Plan_With_Log.snap: Language not supported
- packages/framework/ai-collab/src/test/explicit-strategy/snapshots/Prompt_Regression_Snapshot_Tests/Editing_System_Prompt_With_Plan_With_Log_With_Failed_Edits.snap: Language not supported
function createInvalidModifyFeedbackMsg( | ||
modifyEdit: Modify, | ||
treeNode: TreeNode, | ||
errorType: "NONEXISTENT_FIELD" | "INVALID_TYPE", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT:
I know there's only two types of errorType but can we make this an enum or object?
As we expand the usage of createInvalidModifyFeedbackMsg()
in other treeEdit.types
we should be having more errorTypes
besides NONEXISTENT_FIELD
or INVALID_TYPE
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, should this be 2 separate functions? The shared logic could be extracted into a helper function to avoid code duplication.
} | ||
|
||
messageSuffix = ` The node's field you selected for modification \`${modifyEdit.field}\` does not exist in this nodes schema. The set of available fields for this node are: \`[${nodeFieldNames.map((field) => `'${field}'`).join(", ")}]\`.${closestPossibleMatchForFieldMessage}`; | ||
} else if (errorType === "INVALID_TYPE") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, we can make this else
.
"str": "testStr", | ||
"vectors": [ | ||
{ | ||
"id": (view.root.vectors[0] as Vector).id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see id
is expected for vectors
but not for primitive values such as string
or bool
. Is this expected? Does idGenerator()
generate and embed id
to non-primitive nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our schema explicitly adds an id
field to Vector
(lines 40-52), that's why it only shows up there. It's not that non-primitives get ids by default, it's only those whose schema says they have one.
); | ||
}); | ||
|
||
it("modify edit with non existent field fails", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong, but this test is passing in my local environment.
Also, I think it might be useful to add a test case of a root node with multiple fields of identical Levenshtein distance (so a maybe have fields x1
and x2
and try to modify x3
). And verify that the error log has good readability with multiple values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test needs to pass, otherwise we wouldn't be able to merge this :). The title just means that the test is checking a given scenario fails in expected way (if we're expecting it to fail but it doesn't, then the test itself would fail).
|
||
for (const candidate of possibleMatches) { | ||
const distance = levenshteinDistance(input, candidate); | ||
if (distance < bestDistance) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this condition only honor the first-seen candidate and disregard other candidates with same Levenshtein distance?
Can we return an array containing all the candidates and use string concatenation in the createInvalidModifyFeedbackMsg()
?
} | ||
// If the LLM attempts to use the wrong type for a field, we generate a useful error message that can be used as part of the feedback loop. | ||
const isInvalidTypeError = | ||
error.message.match( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to be pretty fragile - if the error message in tree is updated, this check will begin to silently fail. If we need to be able to match on this specific error, I recommend adding a new Error type to the tree package for this scenario specifically. Then we can against that kind of error in a way that should be detectable through API changes. We can make that error type @internal
for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current unit tests would catch that scenario, since some of them check for the correct generation of INVALID_TYPE
messages. Long-term I like the idea of more specific error types, but they'd probably need to come with type guard functions as well?
@@ -307,6 +339,60 @@ export function applyAgentEdit( | |||
} | |||
} | |||
|
|||
/** | |||
* Produces a useful, context-rich error message for the LLM when the LLM produces an {@link ModifyEdit} that either references a nonexistant field or an invalid type for the selected field. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100% nitpick - take it or leave it 🙂
* Produces a useful, context-rich error message for the LLM when the LLM produces an {@link ModifyEdit} that either references a nonexistant field or an invalid type for the selected field. | |
* Produces a useful, context-rich error message to give as a response to the LLM when it has produced an {@link ModifyEdit} that either references a nonexistant field or an invalid type for the selected field. |
* Computes the Levenshtein distance between two strings. | ||
* The Levenshtein distance between two strings is the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one string into the other. | ||
*/ | ||
export function levenshteinDistance(a: string, b: string): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to implement this ourselves? Is there an external library we could use for this?
|
||
// Initialize the first row of the DP table (representing the cost of transforming an empty string to the first j characters of b) | ||
for (let j = 1; j <= b.length; j++) { | ||
// @ts-ignore - we know the accessed indexes of dp are valid. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Josh to maybe look for a library that implements this if we can find a small one. If we keep the code, let's use @ts-expect-error
for all these instead. Then if we eventually replace the logic with something that TS is ok with, we'll get a complaint about the comments not finding an error anymore and we can remove them.
closestPossibleMatchForFieldMessage = ` If you are sure you are trying to modify this node, did you mean to use the field \`${closestPossibleMatchForField}\` which has the following set of allowed types: \`[${allowedTypeIdentifiers.map((id) => `'${id}'`).join(", ")}]\`?`; | ||
} | ||
|
||
messageSuffix = ` The node's field you selected for modification \`${modifyEdit.field}\` does not exist in this nodes schema. The set of available fields for this node are: \`[${nodeFieldNames.map((field) => `'${field}'`).join(", ")}]\`.${closestPossibleMatchForFieldMessage}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
messageSuffix = ` The node's field you selected for modification \`${modifyEdit.field}\` does not exist in this nodes schema. The set of available fields for this node are: \`[${nodeFieldNames.map((field) => `'${field}'`).join(", ")}]\`.${closestPossibleMatchForFieldMessage}`; | |
messageSuffix = ` The node's field you selected for modification \`${modifyEdit.field}\` does not exist in this node's schema. The set of available fields for this node are: \`[${nodeFieldNames.map((field) => `'${field}'`).join(", ")}]\`.${closestPossibleMatchForFieldMessage}`; |
: [(targetFieldNodeSchema as TreeNodeSchema).identifier]; | ||
|
||
// TODO: If the invalid modification is a new object, it won't be clear what part of the object is invalid for the given type. If we could give some more detailed guidance on what was wrong with the object it would be ideal. | ||
messageSuffix = ` You cannot set the node's field \`${modifyEdit.field}\` to the value \`${modifyEdit.modification}\` with type \`${typeof modifyEdit.modification}\` because this type is incompatible with all of the types allowed by the node's schema. The set of allowed types are \`[${allowedTypeIdentifiers.map((id) => `'${id}'`).join(", ")}]\`.`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
messageSuffix = ` You cannot set the node's field \`${modifyEdit.field}\` to the value \`${modifyEdit.modification}\` with type \`${typeof modifyEdit.modification}\` because this type is incompatible with all of the types allowed by the node's schema. The set of allowed types are \`[${allowedTypeIdentifiers.map((id) => `'${id}'`).join(", ")}]\`.`; | |
messageSuffix = ` You cannot set the node's field \`${modifyEdit.field}\` to the value \`${modifyEdit.modification}\` with type \`${typeof modifyEdit.modification}\` because this type is incompatible with all of the types allowed by the field's schema. The set of allowed types are \`[${allowedTypeIdentifiers.map((id) => `'${id}'`).join(", ")}]\`.`; |
@@ -1,3 +1,4 @@ | |||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of disabling lint rules globally like this without a good explanation. Were there so many instances of it? Also, I think this needs to go under the header so policy checks pass.
); | ||
}); | ||
|
||
it("modify edit with non existent field fails", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test needs to pass, otherwise we wouldn't be able to merge this :). The title just means that the test is checking a given scenario fails in expected way (if we're expecting it to fail but it doesn't, then the test itself would fail).
); | ||
|
||
let closestPossibleMatchForFieldMessage = ""; | ||
if (closestPossibleMatchForField !== "") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will always be true, so maybe remove it? findClosestStringMatch
will always return some field name, and I don't think we can create a field with an empty name in SharedTree. If anything this feels like it could be an assert.
if (closestPossibleMatchForField !== "") { | ||
const targetFieldNodeSchema = nodeFieldSchemas[closestPossibleMatchForField]; | ||
const allowedTypeIdentifiers: string[] = | ||
targetFieldNodeSchema instanceof FieldSchema |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do something different that instanceof
here? Mismatches in the version of SharedTree this library depends on and the one used by the application (which we don't really support, but could happen) will make this check not work as expected.
it("modify edits", () => { | ||
const tree = factory.create( | ||
describe("modify edits", () => { | ||
let tree = factory.create( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: slightly better if we just declare these here but don't initialize them, since the beforeEach
will run anyway and overwrite them before the first test runs.
"str": "testStr", | ||
"vectors": [ | ||
{ | ||
"id": (view.root.vectors[0] as Vector).id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our schema explicitly adds an id
field to Vector
(lines 40-52), that's why it only shows up there. It's not that non-primitives get ids by default, it's only those whose schema says they have one.
"str": "testStr", | ||
"vectors": [ | ||
{ | ||
"id": (view.root.vectors[0] as Vector).id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this works?
"id": (view.root.vectors[0] as Vector).id, | |
"id": vectorId, |
Description
The error messages produced from attempting an invalid modify edit are being used as part of the feedback loop for an LLM so that when it makes an invalid error, it gets a failure message and description as to why it failed. Until now, these error messages were simply what the shared tree library produced, which may be helpful for developers but loses meaningful context for an LLM looking at what it has done in the last via a prompt.
agentEditReducer.ts
- Catches errors during themodify
switch case and replaces the error message with a context rich one.agentEditingReducer.spec.ts
- Refactors tests to have a describe block just for various 'modify' tree edit test cases & adds new cases for when the new error messages would be thrown.utils.ts
- Adds new helper methods for determining what the closest match for an invalid field would be. This helps with providing a 'did you mean to use the field xxx' messageutils.spec.ts
- Adds tests for new helper methods for finding the closest match to a given string.Reviewer Guidance