mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
[Bug, GitHub]: fix missing context in cloud resolver (#10517)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
c0bb84dfa2
commit
9afedea170
@ -9,12 +9,16 @@ from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.queries import (
|
||||
get_review_threads_graphql_query,
|
||||
get_thread_comments_graphql_query,
|
||||
get_thread_from_comment_graphql_query,
|
||||
suggested_task_issue_graphql_query,
|
||||
suggested_task_pr_graphql_query,
|
||||
)
|
||||
from openhands.integrations.service_types import (
|
||||
BaseGitService,
|
||||
Branch,
|
||||
Comment,
|
||||
GitService,
|
||||
InstallationsService,
|
||||
OwnerType,
|
||||
@ -695,6 +699,222 @@ class GitHubService(BaseGitService, GitService, InstallationsService):
|
||||
# Parse the content to extract triggers from frontmatter
|
||||
return self._parse_microagent_content(file_content, file_path)
|
||||
|
||||
async def get_issue_or_pr_comments(
|
||||
self, repository: str, issue_number: int, max_comments: int = 10
|
||||
) -> list[Comment]:
|
||||
"""Get comments for an issue.
|
||||
|
||||
Args:
|
||||
repository: Repository name in format 'owner/repo'
|
||||
issue_number: The issue number
|
||||
discussion_id: Not used for GitHub (kept for compatibility with GitLab)
|
||||
|
||||
Returns:
|
||||
List of Comment objects ordered by creation date
|
||||
"""
|
||||
url = f'{self.BASE_URL}/repos/{repository}/issues/{issue_number}/comments'
|
||||
page = 1
|
||||
all_comments: list[dict] = []
|
||||
|
||||
while len(all_comments) < max_comments:
|
||||
params = {
|
||||
'per_page': 10,
|
||||
'sort': 'created',
|
||||
'direction': 'asc',
|
||||
'page': page,
|
||||
}
|
||||
response, headers = await self._make_request(url, params=params)
|
||||
all_comments.extend(response or [])
|
||||
|
||||
# Parse the Link header for rel="next"
|
||||
link_header = headers.get('Link', '')
|
||||
if 'rel="next"' not in link_header:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
return self._process_raw_comments(all_comments)
|
||||
|
||||
async def get_issue_or_pr_title_and_body(
|
||||
self, repository: str, issue_number: int
|
||||
) -> tuple[str, str]:
|
||||
"""Get the title and body of an issue.
|
||||
|
||||
Args:
|
||||
repository: Repository name in format 'owner/repo'
|
||||
issue_number: The issue number
|
||||
|
||||
Returns:
|
||||
A tuple of (title, body)
|
||||
"""
|
||||
url = f'{self.BASE_URL}/repos/{repository}/issues/{issue_number}'
|
||||
response, _ = await self._make_request(url)
|
||||
title = response.get('title') or ''
|
||||
body = response.get('body') or ''
|
||||
return title, body
|
||||
|
||||
async def get_review_thread_comments(
|
||||
self,
|
||||
comment_id: str,
|
||||
repository: str,
|
||||
pr_number: int,
|
||||
) -> list[Comment]:
|
||||
"""Get all comments in a review thread starting from a specific comment.
|
||||
|
||||
Uses GraphQL to traverse the reply chain from the given comment up to the root
|
||||
comment, then finds the review thread and returns all comments in the thread.
|
||||
|
||||
Args:
|
||||
comment_id: The GraphQL node ID of any comment in the thread
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
|
||||
Returns:
|
||||
List of Comment objects representing the entire thread
|
||||
"""
|
||||
|
||||
# Step 1: Use existing GraphQL query to get the comment and check for replyTo
|
||||
variables = {'commentId': comment_id}
|
||||
data = await self.execute_graphql_query(
|
||||
get_thread_from_comment_graphql_query, variables
|
||||
)
|
||||
|
||||
comment_node = data.get('data', {}).get('node')
|
||||
if not comment_node:
|
||||
return []
|
||||
|
||||
# Step 2: If replyTo exists, traverse to the root comment
|
||||
root_comment_id = comment_id
|
||||
reply_to = comment_node.get('replyTo')
|
||||
if reply_to:
|
||||
root_comment_id = reply_to['id']
|
||||
|
||||
# Step 3: Get all review threads and find the one containing our root comment
|
||||
owner, repo = repository.split('/')
|
||||
thread_id = None
|
||||
after_cursor = None
|
||||
has_next_page = True
|
||||
|
||||
while has_next_page and not thread_id:
|
||||
threads_variables: dict[str, Any] = {
|
||||
'owner': owner,
|
||||
'repo': repo,
|
||||
'number': pr_number,
|
||||
'first': 50,
|
||||
}
|
||||
if after_cursor:
|
||||
threads_variables['after'] = after_cursor
|
||||
|
||||
threads_data = await self.execute_graphql_query(
|
||||
get_review_threads_graphql_query, threads_variables
|
||||
)
|
||||
|
||||
review_threads_data = (
|
||||
threads_data.get('data', {})
|
||||
.get('repository', {})
|
||||
.get('pullRequest', {})
|
||||
.get('reviewThreads', {})
|
||||
)
|
||||
|
||||
review_threads = review_threads_data.get('nodes', [])
|
||||
page_info = review_threads_data.get('pageInfo', {})
|
||||
|
||||
# Search for the thread containing our root comment
|
||||
for thread in review_threads:
|
||||
first_comments = thread.get('comments', {}).get('nodes', [])
|
||||
for first_comment in first_comments:
|
||||
if first_comment.get('id') == root_comment_id:
|
||||
thread_id = thread.get('id')
|
||||
break
|
||||
if thread_id:
|
||||
break
|
||||
|
||||
# Update pagination variables
|
||||
has_next_page = page_info.get('hasNextPage', False)
|
||||
after_cursor = page_info.get('endCursor')
|
||||
|
||||
if not thread_id:
|
||||
# Fallback: return just the comments we found during traversal
|
||||
logger.warning(
|
||||
f'Could not find review thread for comment {comment_id}, returning traversed comments'
|
||||
)
|
||||
return []
|
||||
|
||||
# Step 4: Get all comments from the review thread using the thread ID
|
||||
all_thread_comments = []
|
||||
after_cursor = None
|
||||
has_next_page = True
|
||||
|
||||
while has_next_page:
|
||||
comments_variables: dict[str, Any] = {}
|
||||
comments_variables['threadId'] = thread_id
|
||||
comments_variables['page'] = 50
|
||||
if after_cursor:
|
||||
comments_variables['after'] = after_cursor
|
||||
|
||||
thread_comments_data = await self.execute_graphql_query(
|
||||
get_thread_comments_graphql_query, comments_variables
|
||||
)
|
||||
|
||||
thread_node = thread_comments_data.get('data', {}).get('node')
|
||||
if not thread_node:
|
||||
break
|
||||
|
||||
comments_data = thread_node.get('comments', {})
|
||||
comments_nodes = comments_data.get('nodes', [])
|
||||
page_info = comments_data.get('pageInfo', {})
|
||||
|
||||
all_thread_comments.extend(comments_nodes)
|
||||
|
||||
has_next_page = page_info.get('hasNextPage', False)
|
||||
after_cursor = page_info.get('endCursor')
|
||||
|
||||
return self._process_raw_comments(all_thread_comments)
|
||||
|
||||
def _truncate_comment(
|
||||
self, comment_body: str, max_comment_length: int = 500
|
||||
) -> str:
|
||||
"""Truncate comment body to a maximum length."""
|
||||
if len(comment_body) > max_comment_length:
|
||||
return comment_body[:max_comment_length] + '...'
|
||||
return comment_body
|
||||
|
||||
def _process_raw_comments(
|
||||
self, comments_data: list, max_comments: int = 10
|
||||
) -> list[Comment]:
|
||||
"""Convert raw comment data to Comment objects."""
|
||||
comments: list[Comment] = []
|
||||
for comment in comments_data:
|
||||
author = 'unknown'
|
||||
|
||||
if comment.get('author'):
|
||||
author = comment.get('author', {}).get('login', 'unknown')
|
||||
elif comment.get('user'):
|
||||
author = comment.get('user', {}).get('login', 'unknown')
|
||||
|
||||
comments.append(
|
||||
Comment(
|
||||
id=str(comment.get('id', 'unknown')),
|
||||
body=self._truncate_comment(comment.get('body', '')),
|
||||
author=author,
|
||||
created_at=datetime.fromisoformat(
|
||||
comment.get('createdAt', '').replace('Z', '+00:00')
|
||||
)
|
||||
if comment.get('createdAt')
|
||||
else datetime.fromtimestamp(0),
|
||||
updated_at=datetime.fromisoformat(
|
||||
comment.get('updatedAt', '').replace('Z', '+00:00')
|
||||
)
|
||||
if comment.get('updatedAt')
|
||||
else datetime.fromtimestamp(0),
|
||||
system=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Sort comments by creation date to maintain chronological order
|
||||
comments.sort(key=lambda c: c.created_at)
|
||||
return comments[-max_comments:]
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
|
||||
@ -45,3 +45,80 @@ suggested_task_issue_graphql_query = """
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
get_thread_from_comment_graphql_query = """
|
||||
query GetThreadFromComment($commentId: ID!) {
|
||||
node(id: $commentId) {
|
||||
... on PullRequestReviewComment {
|
||||
id
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
replyTo {
|
||||
id
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
get_review_threads_graphql_query = """
|
||||
query($owner: String!, $repo: String!, $number: Int!, $first: Int = 50, $after: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
reviewThreads(first: $first, after: $after) {
|
||||
nodes {
|
||||
id
|
||||
path
|
||||
isResolved
|
||||
comments(first: 1) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
author {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
get_thread_comments_graphql_query = """
|
||||
query ($threadId: ID!, $page: Int = 50, $after: String) {
|
||||
node(id: $threadId) {
|
||||
... on PullRequestReviewThread {
|
||||
id
|
||||
path
|
||||
isResolved
|
||||
comments(first: $page, after: $after) {
|
||||
nodes {
|
||||
id
|
||||
databaseId
|
||||
body
|
||||
author { login }
|
||||
createdAt
|
||||
}
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@ -727,7 +727,7 @@ class GitLabService(BaseGitService, GitService):
|
||||
continue
|
||||
|
||||
comment = Comment(
|
||||
id=comment_data['id'],
|
||||
id=str(comment_data['id']),
|
||||
body=comment_data['body'],
|
||||
author=comment_data.get('author', {}).get('username', 'unknown'),
|
||||
created_at=datetime.fromisoformat(
|
||||
|
||||
@ -141,7 +141,7 @@ class Repository(BaseModel):
|
||||
|
||||
|
||||
class Comment(BaseModel):
|
||||
id: int
|
||||
id: str
|
||||
body: str
|
||||
author: str
|
||||
created_at: datetime
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
You are requested to fix issue number #{{ issue_number }} in a repository.
|
||||
|
||||
A comment on the issue has been addressed to you.
|
||||
|
||||
# Steps to Handle the Comment
|
||||
|
||||
1. Address the comment. Use the $GITHUB_TOKEN and GitHub API to read issue title, body, and comments if you need more context
|
||||
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
|
||||
3. Run the tests, and if they pass you are done!
|
||||
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
|
||||
|
||||
When you're done, make sure to
|
||||
|
||||
1. Re-read the issue title, body, and comments and make sure that you have successfully implemented all requirements.
|
||||
2. Create a new branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
|
||||
3. Commit your changes with a clear commit message
|
||||
4. Push the branch to GitHub
|
||||
5. Use the `create_pr` tool to open a new PR
|
||||
6. The PR description should:
|
||||
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
|
||||
- Mention that it "fixes" or "closes" the issue number
|
||||
- Include all required sections from the template
|
||||
@ -1 +0,0 @@
|
||||
{{ issue_comment }}
|
||||
@ -0,0 +1,41 @@
|
||||
{% if issue_number %}
|
||||
You are requested to fix issue #{{ issue_number }}: "{{ issue_title }}" in a repository.
|
||||
A comment on the issue has been addressed to you.
|
||||
{% else %}
|
||||
Your task is to fix the issue: "{{ issue_title }}".
|
||||
{% endif %}
|
||||
|
||||
# Issue Body
|
||||
{{ issue_body }}
|
||||
|
||||
{% if previous_comments %}
|
||||
# Previous Comments
|
||||
For reference, here are the previous comments on the issue:
|
||||
|
||||
{% for comment in previous_comments %}
|
||||
- @{{ comment.author }} said:
|
||||
{{ comment.body }}
|
||||
{% if not loop.last %}\n\n{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
# Guidelines
|
||||
|
||||
1. Review the task carefully.
|
||||
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
|
||||
3. Run the tests, and if they pass you are done!
|
||||
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
|
||||
|
||||
# Final Checklist
|
||||
Re-read the issue title, body, and comments and make sure that you have successfully implemented all requirements.
|
||||
|
||||
Use the $GITHUB_TOKEN and GitHub APIs to
|
||||
|
||||
1. Create a new branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
|
||||
2. Commit your changes with a clear commit message
|
||||
3. Push the branch to GitHub
|
||||
4. Use the `create_pr` tool to open a new PR
|
||||
5. The PR description should:
|
||||
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
|
||||
- Mention that it "fixes" or "closes" the issue number
|
||||
- Include all required sections from the template
|
||||
@ -1,17 +0,0 @@
|
||||
Your tasking is to fix an issue in your repository. Do the following
|
||||
|
||||
1. Read the issue body and comments using the $GITHUB_TOKEN and Github API
|
||||
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
|
||||
3. Run the tests, and if they pass you are done!
|
||||
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
|
||||
|
||||
When you're done, make sure to
|
||||
|
||||
1. Create a new branch with a descriptive name (e.g., `openhands/fix-issue-123`)
|
||||
2. Commit your changes with a clear commit message
|
||||
3. Push the branch to GitHub
|
||||
4. Use the `create_pr` tool to open a new PR
|
||||
5. The PR description should:
|
||||
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
|
||||
- Mention that it "fixes" or "closes" the issue number
|
||||
- Include all required sections from the template
|
||||
@ -1 +0,0 @@
|
||||
Please fix issue number #{{ issue_number }} in your repository.
|
||||
@ -0,0 +1,5 @@
|
||||
{% if issue_comment %}
|
||||
{{ issue_comment }}
|
||||
{% else %}
|
||||
Please fix issue number #{{ issue_number }}.
|
||||
{% endif %}
|
||||
@ -1,7 +1,23 @@
|
||||
You are checked out to branch {{ branch_name }}, which has an open PR #{{ pr_number }}.
|
||||
A comment on the PR has been addressed to you. Do NOT respond to this comment via the GitHub API.
|
||||
You are checked out to branch {{ branch_name }}, which has an open PR #{{ pr_number }}: "{{ pr_title }}".
|
||||
A comment on the PR has been addressed to you.
|
||||
|
||||
{% if file_location %} The comment is in the file `{{ file_location }}` on line #{{ line_number }}{% endif %}.
|
||||
# PR Description
|
||||
{{ pr_body }}
|
||||
|
||||
{% if comments %}
|
||||
# Previous Comments
|
||||
You may find these other comments relevant:
|
||||
{% for comment in comments %}
|
||||
- @{{ comment.author }} said at {{ comment.created_at }}:
|
||||
{{ comment.body }}
|
||||
{% if not loop.last %}\n\n{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if file_location %}
|
||||
# Comment location
|
||||
The comment is in the file `{{ file_location }}` on line #{{ line_number }}
|
||||
{% endif %}.
|
||||
|
||||
# Steps to Handle the Comment
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user