Skip to content
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

Feat: Add in ability to delete a single output by prompt name in editor #1465

Conversation

justinkchen
Copy link
Contributor

@justinkchen justinkchen commented Mar 17, 2024

#1451
Screenshot 2024-03-17 at 4 37 15 PM

Adds the ability to delete a single output in the config editor

@justinkchen justinkchen force-pushed the justin_chen-delete-single-prompt-output branch from 8ae319f to 0c0f78d Compare March 17, 2024 01:55
Copy link
Contributor

@saqadri saqadri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great overall!! Nice first diff 😃 -- just one suggestion about the button moved to https://github.com/lastmile-ai/aiconfig/blob/main/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx

<PromptOutputBar />
{!readOnly && prompt.outputs?.length && deleteOutput ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding the button here, can we add it to the prompt menu?

https://github.com/lastmile-ai/aiconfig/blob/main/python/src/aiconfig/editor/client/src/components/prompt/PromptMenuButton.tsx

I imagine we will have other capabilities like Copy/Duplicate prompt which will also be added in the future, so it will prevent too many buttons everywhere

Copy link
Contributor

@saqadri saqadri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ship it! 🚀🚀🚀

@@ -66,8 +68,10 @@ export default memo(function PromptsContainer(props: Props) {
<Flex mt="md">
{!readOnly && (
<PromptMenuButton
prompt={prompt}
promptId={prompt._ui.id}
Copy link
Contributor

@rossdanlm rossdanlm Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we don't need to pass in promptId anymore if passing in prompt

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a comment above -- we shouldn't pass prompt and we can actually just pass callbacks (and a new showDeleteOutput or canDeleteOutput prop)

Comment on lines 156 to 163
if (prompt._ui.id === action.id) {
return {
...prompt,
outputs: undefined,
};
} else {
return prompt;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a utils reducer method called reduceReplacePrompt(). Would prefer to use that to keep things cleaner and more abstracted

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, it will handle selecting the correct prompt by id

Comment on lines 25 to 33
const deleteOutput = useCallback(
async () => await onDeleteOutput(promptId),
[promptId, onDeleteOutput]
);

const deletePrompt = useCallback(
async () => await onDeletePrompt(promptId),
[promptId, onDeletePrompt]
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a front-end thing that Ryan taught me. Since PromptMenuButton is already wrapped with a memo component, it's actually more overhead for us to wrap these sub-functions in a useCallback() hook. Here useCallback() checks for changes to any dependencies, but in our case that's simply changes to the props, which is already memoized in the PromptMenuButton component itself, so in this case it adds overhead. Would be better to either:

  1. Keep inlined
  2. Keep separate but don't wrap with useCallback()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh just noticed as well but we should just construct the onDeletePrompt and onDeleteOutput callbacks in the parent, since the promptId is known there. Then promptId wouldn't even need to be passed, just the callbacks. Similarly that would make PromptMenuButton memoization actually work as intended since the inline function currently prevents memoization from working (new function prop each time). So in PromptsContainer we could have

const promptId = prompt._ui.id;

const onDeletePrompt = props.onDeletePrompt;
const onDeleteOutput = props.onDeleteOutput;

const deletePrompt = useCallback(() => onDeletePrompt(promptId), [onDeletePrompt, promptId]);
const deleteOutput = useCallback(() => onDeleteOutput(promptId), [onDeleteOutput, promptId]);

And they can be passed through the props to PromptMenuButton

Comment on lines 50 to 54
<Menu.Item
icon={<IconEraser size={16} />}
color="white"
onClick={deleteOutput}
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @rholinshead for whether this would work with light mode theming

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think we can remove color here and it will have the correct color we want. To test this, you can set mode="gradio" and themeMode="dark" and themeMode="light" props for AIConfigEditor in the LocalEditor component

@@ -811,9 +812,32 @@ def _op(

signature: dict[str, Type[Any]] = {}
return run_aiconfig_operation_with_request_json(
aiconfig, request_json, f"method_", _op, signature
aiconfig, request_json, f"method_{method_name}", _op, signature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice thanks!

Copy link
Contributor

@rossdanlm rossdanlm Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually upon further inspection, I think this should just be method_name, not f"method_{method_name}", don't think we did this right the first time:

return run_aiconfig_operation_with_op_args(
aiconfig, operation_name, operation, Ok(op_args_ok)
)

).to_flask_format()

signature: dict[str, Type[Any]] = {"prompt_name": str}
operation = make_op_run_method(method_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh cool, TIL that we already have existing API for this:

def delete_output(self, prompt_name: str):
. Thought it was for deleting entire outputs only. We can probly clean this up and delete the line existing_outputs = prompt.outputs. cc @Ankush-lastmile do you remember why this line might have been needed?

"""
Clears a single outputs in the server state's AIConfig based on prompt name.
"""
method_name = MethodName("delete_output")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Based on comment above, we probably don't need to inline this since it's only used once in operation = ... line, but not a blocker

Copy link
Contributor

@rossdanlm rossdanlm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the useCallbacks and potential light mode issue, there are no functional blockers I see on my end. I'm happy to unblock and land for now and we can fix lingering nits in a followup! Let me know whether you prefer to fix these small things in this PR (will wait for you) or do a followup (will merge this for now)

@rossdanlm
Copy link
Contributor

Btw for failing blockers (on autoformatting), you can use either of these commands to reformat and get unblocked (I think, not 100% sure):

  1. black --exclude '/\.sl\b' . --line-length=79
  2. fd --glob '*.py' python/src/aiconfig/editor | xargs python -m 'scripts.lint' --mode=fix --files

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks great, thanks! Just requesting changes for some small but important changes in the comments

@@ -190,7 +190,9 @@ export default memo(function PromptContainer({

{prompt.outputs && prompt.outputs.length > 0 && (
<>
<Flex justify="space-between" direction="column">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this look like / is this needed now that the button is moved to the menu?

onDeletePrompt,
}: {
promptId: string;
prompt: ClientPrompt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing prompt through just to access the output length will mean this component is re-rendered any time any part of the prompt changes (e.g. if typing in the input). That will result in the React memo constantly re-calculating the diff to check if it should re-render, and also re-rendering unnecessarily. Instead, it would be better to have a prop showDeleteOutput which is set to true if the prompt.outputs.length > 0 in the parent component

Comment on lines 25 to 33
const deleteOutput = useCallback(
async () => await onDeleteOutput(promptId),
[promptId, onDeleteOutput]
);

const deletePrompt = useCallback(
async () => await onDeletePrompt(promptId),
[promptId, onDeletePrompt]
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh just noticed as well but we should just construct the onDeletePrompt and onDeleteOutput callbacks in the parent, since the promptId is known there. Then promptId wouldn't even need to be passed, just the callbacks. Similarly that would make PromptMenuButton memoization actually work as intended since the inline function currently prevents memoization from working (new function prop each time). So in PromptsContainer we could have

const promptId = prompt._ui.id;

const onDeletePrompt = props.onDeletePrompt;
const onDeleteOutput = props.onDeleteOutput;

const deletePrompt = useCallback(() => onDeletePrompt(promptId), [onDeletePrompt, promptId]);
const deleteOutput = useCallback(() => onDeleteOutput(promptId), [onDeleteOutput, promptId]);

And they can be passed through the props to PromptMenuButton

Comment on lines 50 to 54
<Menu.Item
icon={<IconEraser size={16} />}
color="white"
onClick={deleteOutput}
>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think we can remove color here and it will have the correct color we want. To test this, you can set mode="gradio" and themeMode="dark" and themeMode="light" props for AIConfigEditor in the LocalEditor component

@@ -34,6 +35,7 @@ type Props = {
) => Promise<void>;
prompts: ClientPrompt[];
runningPromptId?: string;
readOnly?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed as unused, since it's already obtained from context.

@@ -66,8 +68,10 @@ export default memo(function PromptsContainer(props: Props) {
<Flex mt="md">
{!readOnly && (
<PromptMenuButton
prompt={prompt}
promptId={prompt._ui.id}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a comment above -- we shouldn't pass prompt and we can actually just pass callbacks (and a new showDeleteOutput or canDeleteOutput prop)

Comment on lines 156 to 163
if (prompt._ui.id === action.id) {
return {
...prompt,
outputs: undefined,
};
} else {
return prompt;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, it will handle selecting the correct prompt by id

<>
{!readOnly && (
<PromptMenuButton
showDeleteOutput={!!prompt.outputs?.length}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: feel prompt.outputs?.length > 0 is easier to read and I don't really like JS implicitly "falsey-ness" but don't really care

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it actually complains if you use this syntax unelss you do a (prompt.outputs?.length || 0) > 0 it seems, so thats why i kept it with this way

Comment on lines -67 to -72
{!readOnly && (
<PromptMenuButton
promptId={prompt._ui.id}
onDeletePrompt={() => props.onDeletePrompt(prompt._ui.id)}
/>
)}
Copy link
Contributor

@rossdanlm rossdanlm Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omg this had me confused so hard wondering wtf was going on, but this is the PromptsContainer (with an s), not PromptContainer :S

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change, I feel it makes sense to put with the individual prompt component anyways

Comment on lines +76 to +77
onDeletePrompt={props.onDeletePrompt}
onDeleteOutput={props.onDeleteOutput}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

@@ -66,6 +66,7 @@ export function aiConfigToClientConfig(aiconfig: AIConfig): ClientAIConfig {
export type LogEvent =
| "ADD_PROMPT"
| "CLEAR_OUTPUTS"
| "DELETE_OUTPUT"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would like to use wording "CLEAR_OUTPUT" to match with our UI, but again not a blocker or a big deal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think maybe as a bigger refactor of the api? i made it clear output at first but then found that all the apis reference delete output so wanted to stay consistent there.

Copy link
Contributor

@rossdanlm rossdanlm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I think this is great! Will let @rholinshead do a final pass before merging!

Copy link
Member

@rholinshead rholinshead left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice - thanks!

@rholinshead
Copy link
Member

Ah, looks like the formatting lint is still failing. I'll override to merge to unblock and then I can do a quick PR with the formatting. I'd recommend setting up black formatter extension in vs code (if you use it) so that it formats automatically on save instead of needing the command

@rholinshead rholinshead merged commit d57f077 into lastmile-ai:main Mar 19, 2024
1 of 2 checks passed
rholinshead added a commit that referenced this pull request Mar 19, 2024
#1474)

# Remove Unused os Import + Run Black Format + Add Missing Dep & Rebuild

Following up with minor changes from
#1465 (comment)

Mainly, removed unused `os` import and save `server.py` so that black
formatter runs to pass the lint. While testing, noticed lint in
`LocalEditor` for missing dep so added that as well. Just for good
measure, rebuilt the client.

Test Plan:
- Clear output works
- Lint passes on PR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants