mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
13 Commits
fix-git-ch
...
openhands-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef16c6f9a | ||
|
|
10184d2810 | ||
|
|
5e3bd518a6 | ||
|
|
dc0b52456e | ||
|
|
9ee1776a91 | ||
|
|
c6dec197ee | ||
|
|
c74eab9ad0 | ||
|
|
90a93511b6 | ||
|
|
f8fc3820e5 | ||
|
|
e294f7606c | ||
|
|
439e5056d0 | ||
|
|
5a9e7690df | ||
|
|
c483ac0fe5 |
81
.github/workflows/openhands-resolver.yml
vendored
81
.github/workflows/openhands-resolver.yml
vendored
@@ -45,6 +45,87 @@ permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
review-pr:
|
||||
if: |
|
||||
github.event.label.name == 'review-pr'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install poetry
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install poetry
|
||||
|
||||
- name: Install OpenHands dependencies
|
||||
run: |
|
||||
poetry install --without evaluation,llama-index
|
||||
|
||||
- name: Check required environment variables
|
||||
env:
|
||||
LLM_MODEL: ${{ secrets.LLM_MODEL }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
||||
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
|
||||
run: |
|
||||
required_vars=("LLM_MODEL" "LLM_API_KEY" "PAT_TOKEN" "PAT_USERNAME")
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo "Error: Required environment variable $var is not set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||
echo "COMMENT_ID=None" >> $GITHUB_ENV
|
||||
echo "MAX_ITERATIONS=1" >> $GITHUB_ENV
|
||||
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install OpenHands from PR branch
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install git+https://github.com/${{ github.repository }}.git@${{ github.head_ref }}
|
||||
|
||||
- name: Comment on PR with start message
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: ${{ env.ISSUE_NUMBER }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `[OpenHands](https://github.com/All-Hands-AI/OpenHands) started reviewing the PR! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
|
||||
});
|
||||
|
||||
|
||||
- name: Review PR
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
|
||||
LLM_MODEL: ${{ secrets.LLM_MODEL }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
PYTHONPATH: ""
|
||||
run: |
|
||||
cd /tmp && python -m openhands.resolver.review_pr \
|
||||
--repo ${{ github.repository }} \
|
||||
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||
--issue-type ${{ env.ISSUE_TYPE }}
|
||||
|
||||
auto-fix:
|
||||
if: |
|
||||
github.event_name == 'workflow_call' ||
|
||||
|
||||
14
openhands/resolver/prompts/review
Normal file
14
openhands/resolver/prompts/review
Normal file
@@ -0,0 +1,14 @@
|
||||
You are a helpful AI code reviewer. Your task is to review the following pull request:
|
||||
|
||||
{{ body }}
|
||||
|
||||
Please provide a thorough and constructive review that:
|
||||
1. Summarizes the changes and their purpose
|
||||
2. Evaluates code quality, readability, and maintainability
|
||||
3. Identifies potential bugs, edge cases, or performance issues
|
||||
4. Suggests improvements while being respectful and helpful
|
||||
5. Checks for test coverage and documentation
|
||||
6. Verifies the changes match the PR description and requirements
|
||||
|
||||
Format your review with clear sections and use markdown for better readability.
|
||||
Focus on being specific and actionable in your feedback.
|
||||
144
openhands/resolver/review_pr.py
Normal file
144
openhands/resolver/review_pr.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Module for reviewing pull requests."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import jinja2
|
||||
import litellm
|
||||
import requests
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.issue_definitions import PRHandler
|
||||
|
||||
|
||||
def get_pr_diff(owner: str, repo: str, pr_number: int, token: str) -> str:
|
||||
"""Get the diff for a pull request."""
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}'
|
||||
headers = {
|
||||
'Authorization': f'token {token}',
|
||||
'Accept': 'application/vnd.github.v3.diff',
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
def post_review_comment(
|
||||
owner: str, repo: str, pr_number: int, token: str, review: str
|
||||
) -> None:
|
||||
"""Post a review comment on a pull request."""
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/issues/{pr_number}/comments'
|
||||
headers = {
|
||||
'Authorization': f'token {token}',
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
}
|
||||
data = {'body': review}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
def review_pr(
|
||||
owner: str,
|
||||
repo: str,
|
||||
token: str,
|
||||
username: str,
|
||||
output_dir: str,
|
||||
llm_config: LLMConfig,
|
||||
issue_number: int,
|
||||
) -> None:
|
||||
"""Review a pull request.
|
||||
|
||||
Args:
|
||||
owner: Github owner of the repo.
|
||||
repo: Github repository name.
|
||||
token: Github token to access the repository.
|
||||
username: Github username to access the repository.
|
||||
output_dir: Output directory to write the results.
|
||||
llm_config: Configuration for the language model.
|
||||
issue_number: PR number to review.
|
||||
"""
|
||||
# Create output directory
|
||||
pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(output_dir, 'infer_logs')).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
logger.info(f'Using output directory: {output_dir}')
|
||||
|
||||
# Get PR handler
|
||||
pr_handler = PRHandler(owner, repo, token)
|
||||
|
||||
# Get PR details
|
||||
issues: list[GithubIssue] = pr_handler.get_converted_issues(
|
||||
issue_numbers=[issue_number], comment_id=None
|
||||
)
|
||||
pr = issues[0]
|
||||
|
||||
# Get PR diff
|
||||
diff = get_pr_diff(owner, repo, issue_number, token)
|
||||
|
||||
# Load review template
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), 'prompts/review'),
|
||||
'r',
|
||||
) as f:
|
||||
template = jinja2.Template(f.read())
|
||||
|
||||
# Generate review instruction
|
||||
instruction = template.render(
|
||||
body=f'PR #{pr.number}: {pr.title}\n\n{pr.body}\n\nDiff:\n```diff\n{diff}\n```'
|
||||
)
|
||||
|
||||
# Get review from LLM
|
||||
response = litellm.completion(
|
||||
model=llm_config.model,
|
||||
messages=[{'role': 'user', 'content': instruction}],
|
||||
api_key=llm_config.api_key,
|
||||
base_url=llm_config.base_url,
|
||||
)
|
||||
review = response.choices[0].message.content.strip()
|
||||
|
||||
# Post review comment
|
||||
post_review_comment(owner, repo, issue_number, token, review)
|
||||
logger.info('Posted review comment successfully')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
parser = argparse.ArgumentParser(description='Review a pull request.')
|
||||
parser.add_argument(
|
||||
'--repo', type=str, required=True, help='Repository in owner/repo format'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--issue-number', type=int, required=True, help='PR number to review'
|
||||
)
|
||||
parser.add_argument('--issue-type', type=str, required=True, help='Issue type (pr)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Split repo into owner and name
|
||||
owner, repo = args.repo.split('/')
|
||||
|
||||
# Configure LLM
|
||||
llm_config = LLMConfig(
|
||||
model=os.environ['LLM_MODEL'],
|
||||
api_key=os.environ['LLM_API_KEY'],
|
||||
base_url=os.environ.get('LLM_BASE_URL'),
|
||||
)
|
||||
|
||||
# Review PR
|
||||
review_pr(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
token=os.environ['GITHUB_TOKEN'],
|
||||
username=os.environ['GITHUB_USERNAME'],
|
||||
output_dir='/tmp/output',
|
||||
llm_config=llm_config,
|
||||
issue_number=args.issue_number,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2
poetry.lock
generated
2
poetry.lock
generated
@@ -10350,4 +10350,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "ff370b7b5077720b73fe3b90cc1b7fb9c7a262bfbd35885bb717369061e8a466"
|
||||
content-hash = "e7a29a0e396d5515ecb381b0f8bcfc74fd5a1d839884b23bc336db7df312a5a7"
|
||||
|
||||
@@ -63,7 +63,8 @@ opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
|
||||
modal = "^0.66.26"
|
||||
runloop-api-client = "0.10.0"
|
||||
pygithub = "^2.5.0"
|
||||
openhands-aci = "^0.1.1"
|
||||
openhands-aci = "^0.1.1"sue-5219-try2
|
||||
pre-commit = "^4.0.1"
|
||||
python-socketio = "^5.11.4"
|
||||
redis = "^5.2.0"
|
||||
|
||||
|
||||
99
tests/unit/test_review_pr.py
Normal file
99
tests/unit/test_review_pr.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Tests for the review_pr module."""
|
||||
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.review_pr import get_pr_diff, post_review_comment, review_pr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pr() -> GithubIssue:
|
||||
"""Create a mock PR."""
|
||||
return GithubIssue(
|
||||
owner='owner',
|
||||
repo='repo',
|
||||
number=1,
|
||||
title='Test PR',
|
||||
body='Test PR description',
|
||||
thread_comments=None,
|
||||
review_comments=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm_config() -> LLMConfig:
|
||||
"""Create a mock LLM config."""
|
||||
return LLMConfig(
|
||||
model='test-model',
|
||||
api_key='test-key',
|
||||
base_url=None,
|
||||
)
|
||||
|
||||
|
||||
def test_get_pr_diff() -> None:
|
||||
"""Test getting PR diff."""
|
||||
with patch('requests.get') as mock_get:
|
||||
mock_get.return_value.text = 'test diff'
|
||||
diff = get_pr_diff('owner', 'repo', 1, 'token')
|
||||
assert diff == 'test diff'
|
||||
mock_get.assert_called_once_with(
|
||||
'https://api.github.com/repos/owner/repo/pulls/1',
|
||||
headers={
|
||||
'Authorization': 'token token',
|
||||
'Accept': 'application/vnd.github.v3.diff',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_post_review_comment() -> None:
|
||||
"""Test posting review comment."""
|
||||
with patch('requests.post') as mock_post:
|
||||
post_review_comment('owner', 'repo', 1, 'token', 'test review')
|
||||
mock_post.assert_called_once_with(
|
||||
'https://api.github.com/repos/owner/repo/issues/1/comments',
|
||||
headers={
|
||||
'Authorization': 'token token',
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
},
|
||||
json={'body': 'test review'},
|
||||
)
|
||||
|
||||
|
||||
def test_review_pr(mock_pr: GithubIssue, mock_llm_config: LLMConfig) -> None:
|
||||
"""Test reviewing PR."""
|
||||
with (
|
||||
tempfile.TemporaryDirectory() as temp_dir,
|
||||
patch('openhands.resolver.review_pr.PRHandler') as mock_handler,
|
||||
patch('openhands.resolver.review_pr.get_pr_diff') as mock_get_diff,
|
||||
patch('openhands.resolver.review_pr.post_review_comment') as mock_post_comment,
|
||||
patch('litellm.completion') as mock_completion,
|
||||
):
|
||||
# Setup mocks
|
||||
mock_handler.return_value.get_converted_issues.return_value = [mock_pr]
|
||||
mock_get_diff.return_value = 'test diff'
|
||||
mock_completion.return_value.choices = [
|
||||
MagicMock(message=MagicMock(content='test review'))
|
||||
]
|
||||
|
||||
# Run review
|
||||
review_pr(
|
||||
owner='owner',
|
||||
repo='repo',
|
||||
token='token',
|
||||
username='username',
|
||||
output_dir=temp_dir,
|
||||
llm_config=mock_llm_config,
|
||||
issue_number=1,
|
||||
)
|
||||
|
||||
# Verify calls
|
||||
mock_handler.assert_called_once_with('owner', 'repo', 'token')
|
||||
mock_get_diff.assert_called_once_with('owner', 'repo', 1, 'token')
|
||||
mock_completion.assert_called_once()
|
||||
mock_post_comment.assert_called_once_with(
|
||||
'owner', 'repo', 1, 'token', 'test review'
|
||||
)
|
||||
Reference in New Issue
Block a user