Skip to content

Commit

Permalink
Merge commit '302e41d7bb3d5b2b319f1ce2d15e5925dda069a2' into feature/…
Browse files Browse the repository at this point in the history
…tmux-shell
  • Loading branch information
xingyaoww committed Nov 19, 2024
2 parents bd12b99 + 302e41d commit f2d57f9
Show file tree
Hide file tree
Showing 23 changed files with 349 additions and 83 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The agent needs a place to run code and commands. When you run OpenHands on your
to do this by default. But there are other ways of creating a sandbox for the agent.

If you work for a company that provides a cloud-based runtime, you could help us add support for that runtime
by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/runtime.py).
by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/base.py).

#### Testing
When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites.
Expand Down
35 changes: 32 additions & 3 deletions docs/modules/usage/how-to/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,42 @@ This guide explains how to use the OpenHands GitHub Action, both within the Open

## Using the Action in the OpenHands Repository

To use the OpenHands GitHub Action in the OpenHands repository, an OpenHands maintainer can:
To use the OpenHands GitHub Action in a repository, you can:

1. Create an issue in the repository.
2. Add the `fix-me` label to the issue.
3. The action will automatically trigger and attempt to resolve the issue.
2. Add the `fix-me` label to the issue or leave a comment on the issue starting with `@openhands-agent`.

The action will automatically trigger and attempt to resolve the issue.

## Installing the Action in a New Repository

To install the OpenHands GitHub Action in your own repository, follow
the [README for the OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md).

## Usage Tips

### Iterative resolution

1. Create an issue in the repository.
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`
3. Review the attempt to resolve the issue by checking the pull request
4. Follow up with feedback through general comments, review comments, or inline thread comments
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`

### Label versus Macro

- Label (`fix-me`): Requests OpenHands to address the **entire** issue or pull request.
- Macro (`@openhands-agent`): Requests OpenHands to consider only the issue/pull request description and **the specific comment**.

## Advanced Settings

### Add custom repository settings

You can provide custom directions for OpenHands by following the [README for the resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md#providing-custom-instructions).

### Configure custom macro

To customize the default macro (`@openhands-agent`):

1. [Create a repository variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) named `OPENHANDS_MACRO`
2. Assign the variable a custom value
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.14.0",
"version": "0.14.1",
"private": true,
"type": "module",
"engines": {
Expand Down
8 changes: 3 additions & 5 deletions openhands/agenthub/codeact_agent/micro/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* After opening or updating a pull request, send the user a short message with a link to the pull request.
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
```bash
git checkout -b create-widget
git add .
git commit -m "Create widget"
git push origin create-widget
curl -X POST "https://api.github.com/repos/CodeActOrg/openhands/pulls" \
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
curl -X POST "https://api.github.com/repos/$ORG_NAME/$REPO_NAME/pulls" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}'
```
4 changes: 2 additions & 2 deletions openhands/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
LLMNoActionError,
LLMResponseError,
)
from openhands.core.logger import LOG_ALL_EVENTS
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema import AgentState
from openhands.events import EventSource, EventStream, EventStreamSubscriber
Expand Down Expand Up @@ -528,8 +529,7 @@ async def _step(self) -> None:

await self.update_state_after_step()

# Use info level if LOG_ALL_EVENTS is set
log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug'
log_level = 'info' if LOG_ALL_EVENTS else 'debug'
self.log(log_level, str(action), extra={'msg_type': 'ACTION'})

async def _delegate_step(self):
Expand Down
9 changes: 7 additions & 2 deletions openhands/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
LOG_TO_FILE = os.getenv('LOG_TO_FILE', 'False').lower() in ['true', '1', 'yes']
DISABLE_COLOR_PRINTING = False

LOG_ALL_EVENTS = os.getenv('LOG_ALL_EVENTS', 'False').lower() in ['true', '1', 'yes']

ColorType = Literal[
'red',
'green',
Expand Down Expand Up @@ -89,8 +91,11 @@ def format(self, record):
return f'{time_str} - {name_str}:{level_str}: {record.filename}:{record.lineno}\n{msg_type_color}\n{msg}'
return f'{time_str} - {msg_type_color}\n{msg}'
elif msg_type == 'STEP':
msg = '\n\n==============\n' + record.msg + '\n'
return f'{msg}'
if LOG_ALL_EVENTS:
msg = '\n\n==============\n' + record.msg + '\n'
return f'{msg}'
else:
return record.msg
return super().format(record)


Expand Down
5 changes: 4 additions & 1 deletion openhands/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ def create_runtime(
"""Create a runtime for the agent to run on.
config: The app config.
sid: The session id.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
"""
Expand Down Expand Up @@ -105,6 +106,8 @@ async def run_controller(
Args:
config: The app config.
initial_user_action: An Action object containing initial user input
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
runtime: (optional) A runtime for the agent to run on.
agent: (optional) A agent to run.
exit_on_message: quit if agent asks for a message from user (optional)
Expand Down
26 changes: 19 additions & 7 deletions openhands/resolver/examples/openhands-resolver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
types: [labeled]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]

permissions:
contents: write
Expand All @@ -16,16 +20,24 @@ permissions:
jobs:
call-openhands-resolver:
if: |
${{
github.event.label.name == 'fix-me' ||
(github.event_name == 'issue_comment' &&
startsWith(github.event.comment.body, vars.OPENHANDS_MACRO || '@openhands-agent') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER'))
}}
github.event.label.name == 'fix-me' ||
(
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
(startsWith(github.event.comment.body, inputs.macro || '@openhands-agent') || startsWith(github.event.comment.body, inputs.macro || vars.OPENHANDS_MACRO)) &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER')
) ||
(github.event_name == 'pull_request_review' &&
(startsWith(github.event.review.body, inputs.macro || '@openhands-agent') || startsWith(github.event.review.body, inputs.macro || vars.OPENHANDS_MACRO)) &&
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
)
)
uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main
with:
macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }}
max_iterations: 50
max_iterations: ${{ vars.OPENHANDS_MAX_ITER || 50 }}
secrets:
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
Expand Down
35 changes: 29 additions & 6 deletions openhands/resolver/issue_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class IssueHandlerInterface(ABC):
issue_type: ClassVar[str]

@abstractmethod
def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]:
def get_converted_issues(
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
) -> list[GithubIssue]:
"""Download issues from GitHub."""
pass

Expand Down Expand Up @@ -138,13 +140,29 @@ def _get_issue_comments(

return all_comments if all_comments else None

def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]:
def get_converted_issues(
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
) -> list[GithubIssue]:
"""Download issues from Github.
Returns:
List of Github issues.
"""

if not issue_numbers:
raise ValueError('Unspecified issue number')

all_issues = self._download_issues_from_github()
logger.info(f'Limiting resolving to issues {issue_numbers}.')
all_issues = [
issue
for issue in all_issues
if issue['number'] in issue_numbers and 'pull_request' not in issue
]

if len(issue_numbers) == 1 and not all_issues:
raise ValueError(f'Issue {issue_numbers[0]} not found')

converted_issues = []
for issue in all_issues:
if any([issue.get(key) is None for key in ['number', 'title', 'body']]):
Expand All @@ -153,9 +171,6 @@ def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssu
)
continue

if 'pull_request' in issue:
continue

# Get issue thread comments
thread_comments = self._get_issue_comments(
issue['number'], comment_id=comment_id
Expand Down Expand Up @@ -486,8 +501,16 @@ def __get_context_from_external_issues_references(

return closing_issues

def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]:
def get_converted_issues(
self, issue_numbers: list[int] | None = None, comment_id: int | None = None
) -> list[GithubIssue]:
if not issue_numbers:
raise ValueError('Unspecified issue numbers')

all_issues = self._download_issues_from_github()
logger.info(f'Limiting resolving to issues {issue_numbers}.')
all_issues = [issue for issue in all_issues if issue['number'] in issue_numbers]

converted_issues = []
for issue in all_issues:
# For PRs, body can be None
Expand Down
7 changes: 3 additions & 4 deletions openhands/resolver/resolve_all_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,10 @@ async def resolve_issues(
issue_handler = issue_handler_factory(issue_type, owner, repo, token)

# Load dataset
issues: list[GithubIssue] = issue_handler.get_converted_issues()
issues: list[GithubIssue] = issue_handler.get_converted_issues(
issue_numbers=issue_numbers
)

if issue_numbers is not None:
issues = [issue for issue in issues if issue.number in issue_numbers]
logger.info(f'Limiting resolving to issues {issue_numbers}.')
if limit_issues is not None:
issues = issues[:limit_issues]
logger.info(f'Limiting resolving to first {limit_issues} issues.')
Expand Down
7 changes: 2 additions & 5 deletions openhands/resolver/resolve_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,10 @@ async def resolve_issue(

# Load dataset
issues: list[GithubIssue] = issue_handler.get_converted_issues(
comment_id=comment_id
issue_numbers=[issue_number], comment_id=comment_id
)

# Find the specific issue
issue = next((i for i in issues if i.number == issue_number), None)
if not issue:
raise ValueError(f'Issue {issue_number} not found')
issue = issues[0]

if comment_id is not None:
if (
Expand Down
32 changes: 25 additions & 7 deletions openhands/resolver/send_pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def send_pull_request(
pr_type: str,
fork_owner: str | None = None,
additional_message: str | None = None,
target_branch: str | None = None,
) -> str:
if pr_type not in ['branch', 'draft', 'ready']:
raise ValueError(f'Invalid pr_type: {pr_type}')
Expand All @@ -224,12 +225,19 @@ def send_pull_request(
attempt += 1
branch_name = f'{base_branch_name}-try{attempt}'

# Get the default branch
print('Getting default branch...')
response = requests.get(f'{base_url}', headers=headers)
response.raise_for_status()
default_branch = response.json()['default_branch']
print(f'Default branch: {default_branch}')
# Get the default branch or use specified target branch
print('Getting base branch...')
if target_branch:
base_branch = target_branch
# Verify the target branch exists
response = requests.get(f'{base_url}/branches/{target_branch}', headers=headers)
if response.status_code != 200:
raise ValueError(f'Target branch {target_branch} does not exist')
else:
response = requests.get(f'{base_url}', headers=headers)
response.raise_for_status()
base_branch = response.json()['default_branch']
print(f'Base branch: {base_branch}')

# Create and checkout the new branch
print('Creating new branch...')
Expand Down Expand Up @@ -279,7 +287,7 @@ def send_pull_request(
'title': pr_title, # No need to escape title for GitHub API
'body': pr_body,
'head': branch_name,
'base': default_branch,
'base': base_branch,
'draft': pr_type == 'draft',
}
response = requests.post(f'{base_url}/pulls', headers=headers, json=data)
Expand Down Expand Up @@ -435,6 +443,7 @@ def process_single_issue(
llm_config: LLMConfig,
fork_owner: str | None,
send_on_failure: bool,
target_branch: str | None = None,
) -> None:
if not resolver_output.success and not send_on_failure:
print(
Expand Down Expand Up @@ -484,6 +493,7 @@ def process_single_issue(
llm_config=llm_config,
fork_owner=fork_owner,
additional_message=resolver_output.success_explanation,
target_branch=target_branch,
)


Expand All @@ -508,6 +518,7 @@ def process_all_successful_issues(
llm_config,
fork_owner,
False,
None,
)


Expand Down Expand Up @@ -573,6 +584,12 @@ def main():
default=None,
help='Base URL for the LLM model.',
)
parser.add_argument(
'--target-branch',
type=str,
default=None,
help='Target branch to create the pull request against (defaults to repository default branch)',
)
my_args = parser.parse_args()

github_token = (
Expand Down Expand Up @@ -625,6 +642,7 @@ def main():
llm_config,
my_args.fork_owner,
my_args.send_on_failure,
my_args.target_branch,
)


Expand Down
12 changes: 10 additions & 2 deletions openhands/runtime/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,19 @@
}


class RuntimeNotReadyError(Exception):
class RuntimeUnavailableError(Exception):
pass


class RuntimeDisconnectedError(Exception):
class RuntimeNotReadyError(RuntimeUnavailableError):
pass


class RuntimeDisconnectedError(RuntimeUnavailableError):
pass


class RuntimeNotFoundError(RuntimeUnavailableError):
pass


Expand Down
Loading

0 comments on commit f2d57f9

Please sign in to comment.