Skip to content

Commit

Permalink
Add actions for github push and send PR (#1415)
Browse files Browse the repository at this point in the history
* Added a push action

* Tests

* Add tests

* Fix capitalization

* Update

* Fix typo

* Fix integration tests

* Added poetry.lock

* Set lock

* Fix action parsing

* Update integration test output

* Updated prompt

* Update integration test

* Add github token to default config

---------

Co-authored-by: Engel Nyst <[email protected]>
  • Loading branch information
neubig and enyst committed Apr 29, 2024
1 parent 0d224de commit a5f61ca
Show file tree
Hide file tree
Showing 26 changed files with 774 additions and 669 deletions.
Empty file.
21 changes: 21 additions & 0 deletions agenthub/dummy_agent/agent.py
Original file line number Diff line number Diff line change
@@ -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):
"""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
Original file line number Diff line number Diff line change
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',
'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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)

# 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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'

0 comments on commit a5f61ca

Please sign in to comment.