-
Notifications
You must be signed in to change notification settings - Fork 36
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: In safe-mode, asked the user whether they want to continue or cancel before any possible changes are made #122
base: main
Are you sure you want to change the base?
Conversation
…e running shell command
related: it would be nice for toolkits to have a means to declare the system packages they depend on. In this way, we could test for the binary, like |
Hey @codefromthecrypt Thanks for the feedback. Just to confirm whether I understand your comments correctly. Do you mean we can have something smart in toolkit that they can know the packages on my local machine. For example, if I have already have packages on my local machine to achieve the task, the plan won't install another tool to achieve the same task. Is my understanding correct? |
yeah right now, you can run a command and it might try to apt-install a system package. If the toolkits expose any binaries, or "exit code zero" commands they need, goose could obviate attempts to load that toolkit. OTOH, this won't prevent normal prompts from also attempting the same thing in a toolkit. Here's pseudocode of what it might look like --- a/src/goose/toolkit/github.py
+++ b/src/goose/toolkit/github.py
@@ -9,3 +9,7 @@ class Github(Toolkit):
def system(self) -> str:
"""Retrieve detailed configuration and procedural guidelines for GitHub operations"""
return Message.load("prompts/github.jinja").text
+
+ def system_prereqs(self) -> Tuple(str):
+ """List of commands that must complete success for this toolkit to operate"""
+ return ("gh", "gh status")
|
@@ -114,15 +114,16 @@ def get_session_files() -> dict[str, Path]: | |||
@click.option("--profile") | |||
@click.option("--plan", type=click.Path(exists=True)) | |||
@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO") | |||
def session_start(name: Optional[str], profile: str, log_level: str, plan: Optional[str] = None) -> None: | |||
@click.option("--safe-mode", is_flag=True, help="Prompt before executing potentially unsafe commands", default=False) |
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.
wonder if we can borrow from other tools. I think sometimes, if prompt is default, -Y
is overriding it, like apt
-y, --yes, --assume-yes
Automatic yes to prompts; assume "yes" as answer to all prompts and run
non-interactively. If an undesirable situation, such as changing a held package,
trying to install a unauthenticated package or removing an essential package occurs
then apt-get will abort. Configuration Item: APT::Get::Assume-Yes.
--assume-no
Automatic "no" to all prompts. Configuration Item: APT::Get::Assume-No.
Usually, when choosing to name things, I try to borrow from another tool's name that's well used, whether it is vocab or a flag, then you can blame it on them :)
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.
haha, naming is always the hardest part to me. Thank you so much for your advice!
elif isinstance(tool_use.parameters, list): | ||
output = json.dumps(tool.function(*tool_use.parameters)) | ||
if isinstance(tool_use.parameters, dict) or isinstance(tool_use.parameters, list): | ||
function_return_value = tool.function(**tool_use.parameters) |
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.
is this intentional? i think trying to unpack a list
(*
) will throw an error since it's positional arguments not keyword arguments
if isinstance(tool_use.parameters, dict) or isinstance(tool_use.parameters, list): | ||
function_return_value = tool.function(**tool_use.parameters) | ||
if isinstance(function_return_value, dict): | ||
user_decision = function_return_value['user_decision'] if 'user_decision' in function_return_value else None |
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 feel this is easier to follow if we flatten out the conditionals
if isinstance(parameters, dict):
function_return_value = tool.function(**parameters)
elif isinstance(parameters, list):
function_return_value = tool.function(*parameters)
else:
raise ValueError(
f"The provided tool parameters '{parameters}' "
"could not be interpreted as arguments."
)
user_decision = None
if isinstance(function_return_value, dict):
user_decision = function_return_value.get("user_decision")
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.
call_function
also still has a lot of responsibility, which makes it hard to reason/test. i'd take it one step further by separating out each chunk
_execute_tool_function(tool, parameters)
_extract_user_decision(function_return_value)
so then the final resulting ToolResult
reads like this
function_return_value = _execute_tool_function(tool, tool_use.parameters)
user_decision = _extract_user_decision(function_return_value)
output = json.dumps(function_return_value)
RULESTYLE = "bold" | ||
RULEPREFIX = f"[{RULESTYLE}]───[/] " | ||
|
||
TASKS_WITH_EMOJI = {"planned": "⏳", "complete": "✅", "failed": "❌", "in-progress": "🕑", "cancelled": "🚫", "skipped": "⏩"} |
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: could we add a trailing comma so this dict
prints out each line so it's easier to read?
_path.write_text(content) | ||
return "Successfully replaced before with after." | ||
else: | ||
return {"result": "User chooses not to make the change on this file. skip the change", "task_status": "skipped"} |
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 like active voice for this User rejected suggested changes to {file}. Skipping edits.
return None | ||
|
||
def is_in_safe_mode() -> bool: | ||
return os.getenv(GOOSE_SAFE_MODE_ENV, "false") == "true" |
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.
just an opinion i think adding "true" "false" clutters the environment. imo we should should only check if it exists or not (e.g. unset GOOSE_SAFE_MODE
would disable safe mode).
this minimizes user error
Hi @lamchau, Thanks for the review! The code is a bit unorganised now. At the moment with this draft PR I would like to get the feedback about this feature first, like whether it will be useful or it is worthwhile for experiment. If we think it would be useful, I will continue this PR to polish the code. |
@lifeizhou-ap it's worth experimenting with! i currently do this with a |
I think some people would appreciate this feature, as it can be a surprise how "biased to action" goose is out of the box (I like it myself so likely wouldn't use this mode, but if it isn't adding a lot of complexity to it it is an interesting feature to add in as an option) |
Why
At the moment, when the plan is kicked off, the tasks are executed automatically. Users do not have much control to review potential changes that the task will make. It is hard for user to make the decision whether they want to proceed, skip the task or cancel the plan except using
CTRL-C
What
Enable a
safe-mode
(can be renamed) to provide some control for the user to decide whether to continue before running the tools ofshell
patch_file
write_file
Make the diff more readable
Shell
Before the shell script is executed, the user is shown with the explanation of the command. The ai will also determine whether the command will do any changes on the users' computer(via prompt). If it does, the user is will be prompted whether they want to continue to execute the command. If yes, command will be executed. If no, the task will be cancelled and all the subsequence task will be cancelled. (We can also provide
skip
option so that that specific task can be skipped, and polish the experiences)patch_file and write_file
Before changing the file, user are show the diff of the changes in the similar way of
git diff
. The user will be prompted whether they want to change the file. If yes, file will be changed. If no, file won't be changed and will move to the next task. (We can addyes to all
options later on, so goose can remember the user decision in the plan)shell_command.mov
patch_file.mov
write_file.mov
Note
This PR is a draft PR and the changes is a bit unorganised at the moment. The main purpose is to get your feedback about whether the
safe-mode
is useful for our users. I can polish and refactor the code afterwards.