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

Add actions for github push and send PR #1415

Merged
merged 21 commits into from Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Empty file.
21 changes: 21 additions & 0 deletions agenthub/dummy_agent/agent.py
@@ -0,0 +1,21 @@
"""Module for a Dummy agent."""

from opendevin.action.base import NullAction
from opendevin.state import State
from opendevin.action import Action
from typing import List
from opendevin.agent import Agent
from opendevin.controller.agent_controller import AgentController
from opendevin.observation.base import NullObservation, Observation

class DummyAgent(Agent):
li-boxuan marked this conversation as resolved.
Show resolved Hide resolved
"""A dummy agent that does nothing but can be used in testing."""

async def run(self, controller: AgentController) -> Observation:
return NullObservation('')

def step(self, state: State) -> Action:
return NullAction('')

def search_memory(self, query: str) -> List[str]:
return []
34 changes: 22 additions & 12 deletions agenthub/monologue_agent/agent.py
Expand Up @@ -2,7 +2,7 @@
from opendevin.agent import Agent
from opendevin.state import State
from opendevin.llm.llm import LLM
from opendevin.schema import ActionType, ObservationType
from opendevin.schema import ActionType
from opendevin.exceptions import AgentNoInstructionError
from opendevin.schema.config import ConfigType
from opendevin import config
Expand All @@ -15,6 +15,7 @@
FileReadAction,
AgentRecallAction,
BrowseURLAction,
GitHubPushAction,
AgentThinkAction,
)

Expand Down Expand Up @@ -71,6 +72,10 @@
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
'I can browse the web too!',
'If I have done some work and I want to push it to github, I can do that also!',
"Let's do it.",
'PUSH owner/repo branch',
'The repo was successfully pushed to https://github.com/owner/repo/branch',
neubig marked this conversation as resolved.
Show resolved Hide resolved
'And once I have completed my task, I can use the finish action to stop working.',
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
'Very cool. Now to accomplish my task.',
Expand Down Expand Up @@ -152,32 +157,32 @@ def _initialize(self, task: str):
else:
self.memory = None

output_type = ''
previous_action = ''
for thought in INITIAL_THOUGHTS:
thought = thought.replace('$TASK', task)
if output_type != '':
if previous_action != '':
observation: Observation = NullObservation(content='')
if output_type == ObservationType.RUN:
if previous_action in {ActionType.RUN, ActionType.PUSH}:
observation = CmdOutputObservation(
content=thought, command_id=0, command=''
)
elif output_type == ObservationType.READ:
elif previous_action == ActionType.READ:
observation = FileReadObservation(content=thought, path='')
elif output_type == ObservationType.RECALL:
elif previous_action == ActionType.RECALL:
observation = AgentRecallObservation(
content=thought, memories=[])
elif output_type == ObservationType.BROWSE:
elif previous_action == ActionType.BROWSE:
observation = BrowserOutputObservation(
content=thought, url='', screenshot=''
)
self._add_event(observation.to_memory())
output_type = ''
previous_action = ''
else:
action: Action = NullAction()
if thought.startswith('RUN'):
command = thought.split('RUN ')[1]
action = CmdRunAction(command)
output_type = ActionType.RUN
previous_action = ActionType.RUN
elif thought.startswith('WRITE'):
parts = thought.split('WRITE ')[1].split(' > ')
path = parts[1]
Expand All @@ -186,15 +191,20 @@ def _initialize(self, task: str):
elif thought.startswith('READ'):
path = thought.split('READ ')[1]
action = FileReadAction(path=path)
output_type = ActionType.READ
previous_action = ActionType.READ
elif thought.startswith('RECALL'):
query = thought.split('RECALL ')[1]
action = AgentRecallAction(query=query)
output_type = ActionType.RECALL
previous_action = ActionType.RECALL
elif thought.startswith('BROWSE'):
url = thought.split('BROWSE ')[1]
action = BrowseURLAction(url=url)
output_type = ActionType.BROWSE
previous_action = ActionType.BROWSE
elif thought.startswith('PUSH'):
owner_repo, branch = thought.split('PUSH ')[1].split(' ')
owner, repo = owner_repo.split('/')
action = GitHubPushAction(owner=owner, repo=repo, branch=branch)
previous_action = ActionType.PUSH
else:
action = AgentThinkAction(thought=thought)
self._add_event(action.to_memory())
Expand Down
6 changes: 5 additions & 1 deletion agenthub/monologue_agent/utils/prompts.py
Expand Up @@ -46,6 +46,10 @@
* `id` - the ID of the background command to kill
* `browse` - opens a web page. Arguments:
* `url` - the URL to open
* `push` - Push a branch from the current repo to github:
* `owner` - the owner of the repo to push to
* `repo` - the name of the repo to push to
* `branch` - the name of the branch to push
* `recall` - recalls a past memory. Arguments:
* `query` - the query to search for
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
Expand All @@ -54,7 +58,7 @@

%(background_commands)s

You MUST take time to think in between read, write, run, browse, and recall actions.
You MUST take time to think in between read, write, run, browse, push, and recall actions.
You should never act twice in a row without thinking. But if your last several
actions are all "think" actions, you should consider taking a different action.

Expand Down
2 changes: 2 additions & 0 deletions docs/Agents.md
Expand Up @@ -15,6 +15,7 @@ Short term memory is stored as a Monologue object and the model can condense it
`FileReadAction`,
`AgentRecallAction`,
`BrowseURLAction`,
`GithubPushAction`,
`AgentThinkAction`

### Observations:
Expand Down Expand Up @@ -48,6 +49,7 @@ The agent is given its previous action-observation pairs, current task, and hint
`CmdRunAction`,
`CmdKillAction`,
`BrowseURLAction`,
`GithubPushAction`,
`FileReadAction`,
`FileWriteAction`,
`AgentRecallAction`,
Expand Down
2 changes: 2 additions & 0 deletions opendevin/action/__init__.py
Expand Up @@ -2,6 +2,7 @@
from .bash import CmdRunAction, CmdKillAction
from .browse import BrowseURLAction
from .fileop import FileReadAction, FileWriteAction
from .github import GitHubPushAction
from .agent import (
AgentRecallAction,
AgentThinkAction,
Expand All @@ -25,6 +26,7 @@
AgentDelegateAction,
AddTaskAction,
ModifyTaskAction,
GitHubPushAction,
)

ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
Expand Down
158 changes: 158 additions & 0 deletions opendevin/action/github.py
@@ -0,0 +1,158 @@
from dataclasses import dataclass
from opendevin.observation import Observation, AgentErrorObservation
from opendevin.observation.message import AgentMessageObservation
from opendevin.observation.run import CmdOutputObservation
from opendevin.schema import ActionType
from opendevin import config
from typing import TYPE_CHECKING
import requests
import random
import string

from opendevin.schema.config import ConfigType

from .base import ExecutableAction

if TYPE_CHECKING:
from opendevin.controller import AgentController


@dataclass
class GitHubPushAction(ExecutableAction):
"""This pushes the current branch to github.

To use this, you need to set the GITHUB_TOKEN environment variable.
The agent will return a message with a URL that you can click to make a pull
request.

Attributes:
owner: The owner of the source repo
repo: The name of the source repo
branch: The branch to push
action: The action identifier
"""

owner: str
repo: str
branch: str
action: str = ActionType.PUSH

async def run(self, controller: 'AgentController') -> Observation:
github_token = config.get(ConfigType.GITHUB_TOKEN)
if not github_token:
return AgentErrorObservation(
'GITHUB_TOKEN is not set'
)

# Create a random short string to use as a temporary remote
random_remote = ''.join(
['opendevin_temp_'] + random.choices(string.ascii_lowercase, k=5)
)

# Set the temporary remote
new_url = f'https://{github_token}@github.com/{self.owner}/{self.repo}.git'
command = f'git remote add {random_remote} {new_url}'
remote_add_result = controller.action_manager.run_command(
command, background=False
)
if (
not isinstance(remote_add_result, CmdOutputObservation)
or remote_add_result.exit_code != 0
):
return remote_add_result

# Push the branch to the temporary remote
command = f'git push {random_remote} {self.branch}'
push_result = controller.action_manager.run_command(command, background=False)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error handling for push? A common error would be permission related.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now if the command fails it will return the failed command and failure message (after deleting the remote), so I think this might be OK as-is?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good. Rate limiting error should be rare and I don't think we need to handle that. Permission error would have to be resolved by the user anyways. #1290 aims to add support for agent-user interaction, which might be helpful for users to intervene and address permission issues, if any.


# Delete the temporary remote
command = f'git remote remove {random_remote}'
remote_remove_result = controller.action_manager.run_command(
command, background=False
)
if (
not isinstance(remote_remove_result, CmdOutputObservation)
or remote_remove_result.exit_code != 0
):
return remote_remove_result

return push_result

@property
def message(self) -> str:
return f'Pushing branch {self.branch} to {self.owner}/{self.repo}'


@dataclass
class GitHubSendPRAction(ExecutableAction):
"""An action to send a github PR.

To use this, you need to set the GITHUB_TOKEN environment variable.

Attributes:
owner: The owner of the source repo
repo: The name of the source repo
title: The title of the PR
head: The branch to send the PR from
head_repo: The repo to send the PR from
base: The branch to send the PR to
body: The body of the PR
"""

owner: str
repo: str
title: str
head: str
head_repo: str | None
base: str
body: str | None
action: str = ActionType.SEND_PR

async def run(self, controller: 'AgentController') -> Observation:
github_token = config.get(ConfigType.GITHUB_TOKEN)
if not github_token:
return AgentErrorObservation(
'GITHUB_TOKEN is not set'
)

# API URL to create the pull request
url = f'https://api.github.com/repos/{self.owner}/{self.repo}/pulls'

# Headers to authenticate and request JSON responses
headers = {
'Authorization': f'token {github_token}',
'Accept': 'application/vnd.github.v3+json',
}

# Data for the pull request
data = {
'title': self.title,
'head': self.head,
'head_repo': self.head_repo,
'base': self.base,
'body': self.body,
}
data = {k: v for k, v in data.items() if v is not None}

# Make the request
response = requests.post(url, headers=headers, json=data)

# Check for errors
if response.status_code == 201:
return AgentMessageObservation(
'Pull request created successfully!\n'
f'Pull request URL:{response.json()["html_url"]}'
)
else:
return AgentErrorObservation(
'Failed to create pull request\n'
f'Status code: {response.status_code}\n'
f'Response: {response.text}'
)

@property
def message(self) -> str:
return (
f'Sending PR from {self.head_repo}:{self.head} to '
f'{self.owner}:{self.base}'
)
1 change: 1 addition & 0 deletions opendevin/config.py
Expand Up @@ -48,6 +48,7 @@
ConfigType.USE_HOST_NETWORK: 'false',
ConfigType.SSH_HOSTNAME: 'localhost',
ConfigType.DISABLE_COLOR: 'false',
ConfigType.GITHUB_TOKEN: None,
}

config_str = ''
Expand Down
6 changes: 6 additions & 0 deletions opendevin/schema/action.py
Expand Up @@ -73,5 +73,11 @@ class ActionTypeSchema(BaseModel):

CHANGE_TASK_STATE: str = Field(default='change_task_state')

PUSH: str = Field(default='push')
"""Push a branch to github."""

SEND_PR: str = Field(default='send_pr')
"""Send a PR to github."""


ActionType = ActionTypeSchema()
1 change: 1 addition & 0 deletions opendevin/schema/config.py
Expand Up @@ -29,3 +29,4 @@ class ConfigType(str, Enum):
USE_HOST_NETWORK = 'USE_HOST_NETWORK'
SSH_HOSTNAME = 'SSH_HOSTNAME'
DISABLE_COLOR = 'DISABLE_COLOR'
GITHUB_TOKEN = 'GITHUB_TOKEN'