Skip to content

Add AgentRejectAction across multiple modules #1615

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

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agenthub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Here is a list of available Actions, which can be returned by `agent.step()`:
- [`ModifyTaskAction`](../opendevin/action/tasks.py) - Changes the state of a subtask
- [`AgentThinkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history (as well as the chat log)
- [`AgentTalkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history and talk to the user.
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user to enter a new task
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
- [`AgentRejectAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task

You can use `action.to_dict()` and `action_from_dict` to serialize and deserialize actions.

Expand Down
5 changes: 5 additions & 0 deletions agenthub/dummy_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentRejectAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
Expand Down Expand Up @@ -123,6 +124,10 @@ def __init__(self, llm: LLM):
'action': AgentFinishAction(),
'observations': [],
},
{
'action': AgentRejectAction(),
'observations': [],
},
Comment on lines +127 to +130
Copy link
Contributor

@SmartManoj SmartManoj Jul 28, 2024

Choose a reason for hiding this comment

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

The agent_controller will stop after the previous action AgentFinishAction. If this action is placed before, the same will happen. So, is this action needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Probably not needed

]

def step(self, state: State) -> Action:
Expand Down
2 changes: 2 additions & 0 deletions agenthub/micro/_instructions/actions/reject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* `reject` - reject the task. Arguments:
* `outputs` - a dictionary representing the outputs of your task, if any
5 changes: 4 additions & 1 deletion agenthub/micro/commit_writer/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ changes. The commit message should include:
- Optionally, a detailed description if the changes are complex or need further explanation.

You should find the diff using `git diff --cached`, compile a commit message,
and call the `finish` action with `outputs.answer` set to the answer.
and call the `finish` action with `outputs.answer` set to the answer. If current
repo is not a valid git repo, or there is no diff in the staging area, please call
the `reject` action with `outputs.answer` set to the reason.

## History
{{ instructions.history_truncated }}
Expand All @@ -22,6 +24,7 @@ If the last item in the history is an error, you should try to fix it.

## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.reject }}
{{ instructions.actions.finish }}

## Format
Expand Down
5 changes: 4 additions & 1 deletion opendevin/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Action,
AgentDelegateAction,
AgentFinishAction,
AgentRejectAction,
AgentTalkAction,
ChangeAgentStateAction,
MessageAction,
Expand Down Expand Up @@ -281,7 +282,9 @@ async def step(self, i: int) -> bool:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
return False

finished = isinstance(action, AgentFinishAction)
finished = isinstance(action, AgentFinishAction) or isinstance(
action, AgentRejectAction
)
if finished:
self.state.outputs = action.outputs # type: ignore[attr-defined]
logger.info(action, extra={'msg_type': 'INFO'})
Expand Down
5 changes: 5 additions & 0 deletions opendevin/core/schema/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ class ActionTypeSchema(BaseModel):
use the finish action to stop working.
"""

REJECT: str = Field(default='reject')
"""If you're absolutely certain that you cannot complete the task with given requirements,
use the reject action to stop working.
"""

NULL: str = Field(default='null')

SUMMARIZE: str = Field(default='summarize')
Expand Down
3 changes: 3 additions & 0 deletions opendevin/events/action/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
AgentEchoAction,
AgentFinishAction,
AgentRecallAction,
AgentRejectAction,
AgentSummarizeAction,
AgentTalkAction,
AgentThinkAction,
Expand All @@ -30,6 +31,7 @@
AgentThinkAction,
AgentTalkAction,
AgentFinishAction,
AgentRejectAction,
AgentDelegateAction,
AddTaskAction,
ModifyTaskAction,
Expand Down Expand Up @@ -76,6 +78,7 @@ def action_from_dict(action: dict) -> Action:
'AgentThinkAction',
'AgentTalkAction',
'AgentFinishAction',
'AgentRejectAction',
'AgentDelegateAction',
'AgentEchoAction',
'AgentSummarizeAction',
Expand Down
11 changes: 11 additions & 0 deletions opendevin/events/action/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ def message(self) -> str:
return "All done! What's next on the agenda?"


@dataclass
class AgentRejectAction(Action):
outputs: Dict = field(default_factory=dict)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this should have an arbitrary dict of outputs, the way finish does. All we want is a reason

Copy link
Collaborator Author

@li-boxuan li-boxuan May 8, 2024

Choose a reason for hiding this comment

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

I see where you are coming from, but I have a few counter-arguments:

  1. An agent can choose to have more than just a reason attribute. For example, it can suggest how to fix the task in addition to a reason.
  2. Both AgentRejectAction and AgentFinishAction are termination actions, so it's better for them to have the same interface IMO.
  3. By design, a micro-agent has a schema for inputs and outputs. We don't really enforce it at the moment (i.e. a micro-agent can finish with whatever outputs type), but I feel like we should do it sooner or later. If AgentRejectAction and AgentFinishAction could have different schema for outputs, things will become trickier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will create a follow-up to enforce the schema for micro-agents. We could discuss if we want to continue this approach there.

thought: str = ''
action: str = ActionType.REJECT

@property
def message(self) -> str:
return 'Task is rejected by the agent.'


@dataclass
class AgentDelegateAction(Action):
agent: str
Expand Down
62 changes: 38 additions & 24 deletions tests/unit/test_action_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentRejectAction,
AgentThinkAction,
BrowseURLAction,
CmdKillAction,
Expand All @@ -18,99 +19,112 @@
def serialization_deserialization(original_action_dict, cls):
action_instance = action_from_dict(original_action_dict)
assert isinstance(
action_instance, Action), 'The action instance should be an instance of Action.'
action_instance, Action
), 'The action instance should be an instance of Action.'
assert isinstance(
action_instance, cls), f'The action instance should be an instance of {cls.__name__}.'
action_instance, cls
), f'The action instance should be an instance of {cls.__name__}.'
serialized_action_dict = action_instance.to_dict()
serialized_action_memory = action_instance.to_memory()
serialized_action_dict.pop('message')
assert serialized_action_dict == original_action_dict, 'The serialized action should match the original action dict.'
assert serialized_action_memory == original_action_dict, 'The serialized action in memory should match the original action dict.'
assert (
serialized_action_dict == original_action_dict
), 'The serialized action should match the original action dict.'
assert (
serialized_action_memory == original_action_dict
), 'The serialized action in memory should match the original action dict.'


def test_agent_think_action_serialization_deserialization():
original_action_dict = {
'action': 'think',
'args': {'thought': 'This is a test.'}
}
original_action_dict = {'action': 'think', 'args': {'thought': 'This is a test.'}}
serialization_deserialization(original_action_dict, AgentThinkAction)


def test_agent_recall_action_serialization_deserialization():
original_action_dict = {
'action': 'recall',
'args': {'query': 'Test query.', 'thought': ''}
'args': {'query': 'Test query.', 'thought': ''},
}
serialization_deserialization(original_action_dict, AgentRecallAction)


def test_agent_finish_action_serialization_deserialization():
original_action_dict = {
'action': 'finish',
'args': {'outputs': {}, 'thought': ''}
}
original_action_dict = {'action': 'finish', 'args': {'outputs': {}, 'thought': ''}}
serialization_deserialization(original_action_dict, AgentFinishAction)


def test_agent_reject_action_serialization_deserialization():
original_action_dict = {'action': 'reject', 'args': {'outputs': {}, 'thought': ''}}
serialization_deserialization(original_action_dict, AgentRejectAction)


def test_cmd_kill_action_serialization_deserialization():
original_action_dict = {
'action': 'kill',
'args': {'id': '1337', 'thought': ''}
}
original_action_dict = {'action': 'kill', 'args': {'id': '1337', 'thought': ''}}
serialization_deserialization(original_action_dict, CmdKillAction)


def test_cmd_run_action_serialization_deserialization():
original_action_dict = {
'action': 'run',
'args': {'command': 'echo "Hello world"', 'background': True, 'thought': ''}
'args': {'command': 'echo "Hello world"', 'background': True, 'thought': ''},
}
serialization_deserialization(original_action_dict, CmdRunAction)


def test_browse_url_action_serialization_deserialization():
original_action_dict = {
'action': 'browse',
'args': {'thought': '', 'url': 'https://www.example.com'}
'args': {'thought': '', 'url': 'https://www.example.com'},
}
serialization_deserialization(original_action_dict, BrowseURLAction)


def test_github_push_action_serialization_deserialization():
original_action_dict = {
'action': 'push',
'args': {'owner': 'myname', 'repo': 'myrepo', 'branch': 'main'}
'args': {'owner': 'myname', 'repo': 'myrepo', 'branch': 'main'},
}
serialization_deserialization(original_action_dict, GitHubPushAction)


def test_file_read_action_serialization_deserialization():
original_action_dict = {
'action': 'read',
'args': {'path': '/path/to/file.txt', 'start': 0, 'end': -1, 'thought': 'None'}
'args': {'path': '/path/to/file.txt', 'start': 0, 'end': -1, 'thought': 'None'},
}
serialization_deserialization(original_action_dict, FileReadAction)


def test_file_write_action_serialization_deserialization():
original_action_dict = {
'action': 'write',
'args': {'path': '/path/to/file.txt', 'content': 'Hello world', 'start': 0, 'end': 1, 'thought': 'None'}
'args': {
'path': '/path/to/file.txt',
'content': 'Hello world',
'start': 0,
'end': 1,
'thought': 'None',
},
}
serialization_deserialization(original_action_dict, FileWriteAction)


def test_add_task_action_serialization_deserialization():
original_action_dict = {
'action': 'add_task',
'args': {'parent': 'Test parent', 'goal': 'Test goal', 'subtasks': [], 'thought': ''}
'args': {
'parent': 'Test parent',
'goal': 'Test goal',
'subtasks': [],
'thought': '',
},
}
serialization_deserialization(original_action_dict, AddTaskAction)


def test_modify_task_action_serialization_deserialization():
original_action_dict = {
'action': 'modify_task',
'args': {'id': 1, 'state': 'Test state.', 'thought': ''}
'args': {'id': 1, 'state': 'Test state.', 'thought': ''},
}
serialization_deserialization(original_action_dict, ModifyTaskAction)