mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 628311d701 |
@@ -115,6 +115,311 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ github.event.review.body }}" ]; then
|
||||||
|
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||||
|
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||||
|
elif [ -n "${{ github.event.issue.pull_request }}" ]; then
|
||||||
|
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
|
||||||
|
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||||
|
elif [ -n "${{ github.event.pull_request.number }}" ]; then
|
||||||
|
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||||
|
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
|
||||||
|
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${{ github.event.review.body }}" ]; then
|
||||||
|
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
|
||||||
|
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Comment on issue with start message
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
const issueType = process.env.ISSUE_TYPE;
|
||||||
|
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 fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Install OpenHands
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.label.name }}" == "fix-me-experimental" ]] ||
|
||||||
|
([[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]] &&
|
||||||
|
[[ "${{ github.event.comment.body }}" == "@openhands-agent-exp"* ]]) ||
|
||||||
|
([[ "${{ github.event_name }}" == "pull_request_review" ]] &&
|
||||||
|
[[ "${{ github.event.review.body }}" == "@openhands-agent-exp"* ]]); then
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install git+https://github.com/all-hands-ai/openhands.git
|
||||||
|
else
|
||||||
|
python -m pip install --upgrade -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Attempt to resolve issue
|
||||||
|
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.resolve_issue \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||||
|
--issue-type ${{ env.ISSUE_TYPE }} \
|
||||||
|
--max-iterations ${{ env.MAX_ITERATIONS }} \
|
||||||
|
--comment-id ${{ env.COMMENT_ID }}
|
||||||
|
|
||||||
|
- name: Check resolution result
|
||||||
|
id: check_result
|
||||||
|
run: |
|
||||||
|
if cd /tmp && grep -q '"success":true' output/output.jsonl; then
|
||||||
|
echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload output.jsonl as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always() # Upload even if the previous steps fail
|
||||||
|
with:
|
||||||
|
name: resolver-output
|
||||||
|
path: /tmp/output/output.jsonl
|
||||||
|
retention-days: 30 # Keep the artifact for 30 days
|
||||||
|
|
||||||
|
- name: Create draft PR or push branch
|
||||||
|
if: always() # Create PR or branch even if the previous steps fail
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT_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: |
|
||||||
|
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
|
||||||
|
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||||
|
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||||
|
--pr-type draft | tee pr_result.txt && \
|
||||||
|
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
|
||||||
|
else
|
||||||
|
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||||
|
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||||
|
--pr-type branch \
|
||||||
|
--send-on-failure | tee branch_result.txt && \
|
||||||
|
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Comment on issue
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
if: always() # Comment on issue even if the previous steps fail
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||||
|
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
|
||||||
|
|
||||||
|
let prNumber = '';
|
||||||
|
let branchName = '';
|
||||||
|
let logContent = '';
|
||||||
|
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (success){
|
||||||
|
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
|
||||||
|
} else {
|
||||||
|
logContent = fs.readFileSync('/tmp/branch_result.txt', 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading results file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (success) {
|
||||||
|
prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim();
|
||||||
|
} else {
|
||||||
|
branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logContent.includes(noChangesMessage)) {
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: issueNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
|
||||||
|
});
|
||||||
|
} else if (success && prNumber) {
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: issueNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
|
||||||
|
});
|
||||||
|
} else if (!success && branchName) {
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: issueNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
github.rest.issues.createComment({
|
||||||
|
issue_number: issueNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
review-pr:
|
||||||
|
if: github.event.label.name == 'review-pr'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Get latest versions and create requirements.txt
|
||||||
|
run: |
|
||||||
|
python -m pip index versions openhands-ai > openhands_versions.txt
|
||||||
|
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
|
||||||
|
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
|
||||||
|
cat requirements.txt
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
|
||||||
|
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||||
|
|
||||||
|
- 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 "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
|
||||||
|
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- 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: Install OpenHands
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
- 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.resolve_issue \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||||
|
--issue-type ${{ env.ISSUE_TYPE }} \
|
||||||
|
--max-iterations ${{ env.MAX_ITERATIONS }} \
|
||||||
|
--prompt-template pr-review \
|
||||||
|
--comment-id ${{ env.COMMENT_ID }}
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Get latest versions and create requirements.txt
|
||||||
|
run: |
|
||||||
|
python -m pip index versions openhands-ai > openhands_versions.txt
|
||||||
|
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
|
||||||
|
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
|
||||||
|
cat requirements.txt
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
if: |
|
||||||
|
!(
|
||||||
|
github.event.label.name == 'fix-me-experimental' ||
|
||||||
|
(
|
||||||
|
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
|
||||||
|
contains(github.event.comment.body, '@openhands-agent-exp')
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
github.event_name == 'pull_request_review' &&
|
||||||
|
contains(github.event.review.body, '@openhands-agent-exp')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
|
||||||
|
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||||
|
|
||||||
|
- 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
|
- name: Set environment variables
|
||||||
run: |
|
run: |
|
||||||
if [ -n "${{ github.event.review.body }}" ]; then
|
if [ -n "${{ github.event.review.body }}" ]; then
|
||||||
|
|||||||
@@ -204,14 +204,31 @@ class IssueHandler(IssueHandlerInterface):
|
|||||||
issue.thread_comments
|
issue.thread_comments
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Format review comments if they exist
|
||||||
|
review_comments = ''
|
||||||
|
if issue.review_comments:
|
||||||
|
review_comments = '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format review threads if they exist
|
||||||
|
review_threads = ''
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
review_threads = '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
images.extend(self._extract_image_urls(issue.body))
|
images.extend(self._extract_image_urls(issue.body))
|
||||||
images.extend(self._extract_image_urls(thread_context))
|
images.extend(self._extract_image_urls(thread_context))
|
||||||
|
images.extend(self._extract_image_urls(review_comments))
|
||||||
|
images.extend(self._extract_image_urls(review_threads))
|
||||||
|
|
||||||
template = jinja2.Template(prompt_template)
|
template = jinja2.Template(prompt_template)
|
||||||
return (
|
return (
|
||||||
template.render(
|
template.render(
|
||||||
body=issue.title + '\n\n' + issue.body + thread_context,
|
body=issue.title + '\n\n' + issue.body + thread_context + review_comments + review_threads,
|
||||||
repo_instruction=repo_instruction,
|
repo_instruction=repo_instruction,
|
||||||
),
|
),
|
||||||
images,
|
images,
|
||||||
@@ -229,6 +246,19 @@ class IssueHandler(IssueHandlerInterface):
|
|||||||
issue.thread_comments
|
issue.thread_comments
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Include review comments in the prompt if they exist
|
||||||
|
if issue.review_comments:
|
||||||
|
issue_context += '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include review threads in the prompt if they exist
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
issue_context += '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
os.path.dirname(__file__),
|
os.path.dirname(__file__),
|
||||||
@@ -262,6 +292,103 @@ class PRHandler(IssueHandler):
|
|||||||
super().__init__(owner, repo, token)
|
super().__init__(owner, repo, token)
|
||||||
self.download_url = 'https://api.github.com/repos/{}/{}/pulls'
|
self.download_url = 'https://api.github.com/repos/{}/{}/pulls'
|
||||||
|
|
||||||
|
def get_instruction(
|
||||||
|
self,
|
||||||
|
issue: GithubIssue,
|
||||||
|
prompt_template: str,
|
||||||
|
repo_instruction: str | None = None,
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
"""Generate instruction for the agent."""
|
||||||
|
from openhands.resolver.pr_review import get_pr_review_instruction
|
||||||
|
return get_pr_review_instruction(issue, prompt_template, repo_instruction)
|
||||||
|
|
||||||
|
def guess_success(
|
||||||
|
self, issue: GithubIssue, history: list[Event], llm_config: LLMConfig
|
||||||
|
) -> tuple[bool, None | list[bool], str]:
|
||||||
|
"""Guess if the PR review is successful based on the history and the issue description."""
|
||||||
|
from openhands.resolver.pr_review import guess_pr_review_success
|
||||||
|
return guess_pr_review_success(issue, history, llm_config)
|
||||||
|
|
||||||
|
# Format review comments if they exist
|
||||||
|
review_comments = ''
|
||||||
|
if issue.review_comments:
|
||||||
|
review_comments = '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format review threads if they exist
|
||||||
|
review_threads = ''
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
review_threads = '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
images.extend(self._extract_image_urls(issue.body))
|
||||||
|
images.extend(self._extract_image_urls(thread_context))
|
||||||
|
images.extend(self._extract_image_urls(review_comments))
|
||||||
|
images.extend(self._extract_image_urls(review_threads))
|
||||||
|
|
||||||
|
template = jinja2.Template(prompt_template)
|
||||||
|
return (
|
||||||
|
template.render(
|
||||||
|
body=issue.title + '\n\n' + issue.body + thread_context + review_comments + review_threads,
|
||||||
|
repo_instruction=repo_instruction,
|
||||||
|
),
|
||||||
|
images,
|
||||||
|
)
|
||||||
|
|
||||||
|
def guess_success(
|
||||||
|
self, issue: GithubIssue, history: list[Event], llm_config: LLMConfig
|
||||||
|
) -> tuple[bool, None | list[bool], str]:
|
||||||
|
"""Guess if the PR review is successful based on the history and the issue description."""
|
||||||
|
last_message = history[-1].message
|
||||||
|
# Include thread comments in the prompt if they exist
|
||||||
|
issue_context = issue.body
|
||||||
|
if issue.thread_comments:
|
||||||
|
issue_context += '\n\nIssue Thread Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.thread_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include review comments in the prompt if they exist
|
||||||
|
if issue.review_comments:
|
||||||
|
issue_context += '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include review threads in the prompt if they exist
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
issue_context += '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'prompts/guess_success/issue-success-check.jinja',
|
||||||
|
),
|
||||||
|
'r',
|
||||||
|
) as f:
|
||||||
|
template = jinja2.Template(f.read())
|
||||||
|
prompt = template.render(issue_context=issue_context, last_message=last_message)
|
||||||
|
|
||||||
|
response = litellm.completion(
|
||||||
|
model=llm_config.model,
|
||||||
|
messages=[{'role': 'user', 'content': prompt}],
|
||||||
|
api_key=llm_config.api_key,
|
||||||
|
base_url=llm_config.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = response.choices[0].message.content.strip()
|
||||||
|
pattern = r'--- success\n*(true|false)\n*--- explanation*\n((?:.|\n)*)'
|
||||||
|
match = re.search(pattern, answer)
|
||||||
|
if match:
|
||||||
|
return match.group(1).lower() == 'true', None, match.group(2)
|
||||||
|
|
||||||
|
return False, None, f'Failed to decode answer from LLM response: {answer}'
|
||||||
|
|
||||||
def __download_pr_metadata(
|
def __download_pr_metadata(
|
||||||
self, pull_number: int, comment_id: int | None = None
|
self, pull_number: int, comment_id: int | None = None
|
||||||
) -> tuple[list[str], list[int], list[str], list[ReviewThread], list[str]]:
|
) -> tuple[list[str], list[int], list[str], list[ReviewThread], list[str]]:
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""PR review functionality for OpenHands."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
from openhands.core.config import LLMConfig
|
||||||
|
from openhands.events.event import Event
|
||||||
|
from openhands.resolver.github_issue import GithubIssue
|
||||||
|
|
||||||
|
|
||||||
|
def get_pr_review_instruction(
|
||||||
|
issue: GithubIssue,
|
||||||
|
prompt_template: str,
|
||||||
|
repo_instruction: str | None = None,
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
"""Generate instruction for the PR review agent."""
|
||||||
|
# Format thread comments if they exist
|
||||||
|
thread_context = ''
|
||||||
|
if issue.thread_comments:
|
||||||
|
thread_context = '\n\nIssue Thread Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.thread_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format review comments if they exist
|
||||||
|
review_comments = ''
|
||||||
|
if issue.review_comments:
|
||||||
|
review_comments = '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format review threads if they exist
|
||||||
|
review_threads = ''
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
review_threads = '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
images.extend(_extract_image_urls(issue.body))
|
||||||
|
images.extend(_extract_image_urls(thread_context))
|
||||||
|
images.extend(_extract_image_urls(review_comments))
|
||||||
|
images.extend(_extract_image_urls(review_threads))
|
||||||
|
|
||||||
|
template = jinja2.Template(prompt_template)
|
||||||
|
return (
|
||||||
|
template.render(
|
||||||
|
body=issue.title + '\n\n' + issue.body + thread_context + review_comments + review_threads,
|
||||||
|
repo_instruction=repo_instruction,
|
||||||
|
),
|
||||||
|
images,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def guess_pr_review_success(
|
||||||
|
issue: GithubIssue, history: list[Event], llm_config: LLMConfig
|
||||||
|
) -> tuple[bool, None | list[bool], str]:
|
||||||
|
"""Guess if the PR review is successful based on the history and the issue description."""
|
||||||
|
last_message = history[-1].message
|
||||||
|
# Include thread comments in the prompt if they exist
|
||||||
|
issue_context = issue.body
|
||||||
|
if issue.thread_comments:
|
||||||
|
issue_context += '\n\nIssue Thread Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.thread_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include review comments in the prompt if they exist
|
||||||
|
if issue.review_comments:
|
||||||
|
issue_context += '\n\nReview Comments:\n' + '\n---\n'.join(
|
||||||
|
issue.review_comments
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include review threads in the prompt if they exist
|
||||||
|
if issue.review_threads:
|
||||||
|
thread_messages = []
|
||||||
|
for thread in issue.review_threads:
|
||||||
|
thread_messages.append(f'File: {", ".join(thread.files)}\n{thread.comment}')
|
||||||
|
issue_context += '\n\nReview Threads:\n' + '\n---\n'.join(thread_messages)
|
||||||
|
|
||||||
|
with open(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'prompts/guess_success/issue-success-check.jinja',
|
||||||
|
),
|
||||||
|
'r',
|
||||||
|
) as f:
|
||||||
|
template = jinja2.Template(f.read())
|
||||||
|
prompt = template.render(issue_context=issue_context, last_message=last_message)
|
||||||
|
|
||||||
|
response = litellm.completion(
|
||||||
|
model=llm_config.model,
|
||||||
|
messages=[{'role': 'user', 'content': prompt}],
|
||||||
|
api_key=llm_config.api_key,
|
||||||
|
base_url=llm_config.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = response.choices[0].message.content.strip()
|
||||||
|
pattern = r'--- success\n*(true|false)\n*--- explanation*\n((?:.|\n)*)'
|
||||||
|
match = re.search(pattern, answer)
|
||||||
|
if match:
|
||||||
|
return match.group(1).lower() == 'true', None, match.group(2)
|
||||||
|
|
||||||
|
return False, None, f'Failed to decode answer from LLM response: {answer}'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Please review the following pull request changes and provide a comprehensive code review. Focus on:
|
||||||
|
|
||||||
|
1. Code quality and best practices
|
||||||
|
2. Potential bugs or issues
|
||||||
|
3. Performance implications
|
||||||
|
4. Security concerns
|
||||||
|
5. Test coverage
|
||||||
|
6. Documentation completeness
|
||||||
|
|
||||||
|
Pull Request Details:
|
||||||
|
{{ body }}
|
||||||
|
|
||||||
|
{% if repo_instruction %}
|
||||||
|
Repository-specific instructions:
|
||||||
|
{{ repo_instruction }}
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openhands.core.config import LLMConfig
|
||||||
|
from openhands.resolver.github_issue import GithubIssue, ReviewThread
|
||||||
|
from openhands.resolver.issue_definitions import PRHandler
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pr_handler():
|
||||||
|
return PRHandler('test-owner', 'test-repo', 'test-token')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_pr():
|
||||||
|
return GithubIssue(
|
||||||
|
owner='test-owner',
|
||||||
|
repo='test-repo',
|
||||||
|
number=1,
|
||||||
|
title='Test PR',
|
||||||
|
body='This is a test PR with some changes.',
|
||||||
|
thread_comments=['LGTM!'],
|
||||||
|
review_comments=['Please fix this issue.'],
|
||||||
|
review_threads=[
|
||||||
|
ReviewThread(
|
||||||
|
comment='Fix this code style issue',
|
||||||
|
files=['src/main.py'],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
head_branch='feature-branch',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def llm_config():
|
||||||
|
return LLMConfig(
|
||||||
|
model='test-model',
|
||||||
|
api_key='test-key',
|
||||||
|
base_url='test-url',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pr_review_instruction(pr_handler, test_pr):
|
||||||
|
with patch('jinja2.Template') as mock_template:
|
||||||
|
mock_template.return_value.render.return_value = 'Test instruction'
|
||||||
|
instruction, images = pr_handler.get_instruction(
|
||||||
|
test_pr,
|
||||||
|
'pr-review',
|
||||||
|
repo_instruction='Test repo instruction',
|
||||||
|
)
|
||||||
|
assert instruction == 'Test instruction'
|
||||||
|
assert images == []
|
||||||
|
mock_template.return_value.render.assert_called_once_with(
|
||||||
|
body='Test PR\n\nThis is a test PR with some changes.\n\nIssue Thread Comments:\nLGTM!\n\nReview Comments:\nPlease fix this issue.\n\nReview Threads:\nFile: src/main.py\nFix this code style issue',
|
||||||
|
repo_instruction='Test repo instruction',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pr_review_success_guess(pr_handler, test_pr, llm_config):
|
||||||
|
with patch('litellm.completion') as mock_completion:
|
||||||
|
mock_completion.return_value.choices = [
|
||||||
|
MagicMock(message=MagicMock(content='--- success\ntrue\n--- explanation\nAll issues fixed.'))
|
||||||
|
]
|
||||||
|
success, comment_success, explanation = pr_handler.guess_success(
|
||||||
|
test_pr,
|
||||||
|
[MagicMock(message='Fixed all issues.')],
|
||||||
|
llm_config,
|
||||||
|
)
|
||||||
|
assert success is True
|
||||||
|
assert comment_success is None
|
||||||
|
assert explanation == 'All issues fixed.'
|
||||||
|
mock_completion.assert_called_once_with(
|
||||||
|
model='test-model',
|
||||||
|
messages=[{'role': 'user', 'content': mock_completion.call_args[1]['messages'][0]['content']}],
|
||||||
|
api_key='test-key',
|
||||||
|
base_url='test-url',
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user