mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
feat: add github pr review tools [closes OPE-24] (#1104)
* Add GitHub PR review tools (list, get, create, update, dismiss, submit, list comments) and bind them to the agent * format n lint --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Aran Yogesh <yogesh.mahendran@langchain.dev>
This commit is contained in:
parent
91348aeb7c
commit
267b2fd011
3 changed files with 330 additions and 0 deletions
|
|
@ -35,7 +35,10 @@ from .middleware import (
|
|||
from .prompt import construct_system_prompt
|
||||
from .tools import (
|
||||
commit_and_open_pr,
|
||||
create_pr_review,
|
||||
dismiss_pr_review,
|
||||
fetch_url,
|
||||
get_pr_review,
|
||||
github_comment,
|
||||
http_request,
|
||||
linear_comment,
|
||||
|
|
@ -45,7 +48,11 @@ from .tools import (
|
|||
linear_get_issue_comments,
|
||||
linear_list_teams,
|
||||
linear_update_issue,
|
||||
list_pr_review_comments,
|
||||
list_pr_reviews,
|
||||
slack_thread_reply,
|
||||
submit_pr_review,
|
||||
update_pr_review,
|
||||
)
|
||||
from .utils.auth import resolve_github_token
|
||||
from .utils.model import make_model
|
||||
|
|
@ -413,6 +420,13 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
|||
linear_update_issue,
|
||||
slack_thread_reply,
|
||||
github_comment,
|
||||
list_pr_reviews,
|
||||
get_pr_review,
|
||||
create_pr_review,
|
||||
update_pr_review,
|
||||
dismiss_pr_review,
|
||||
submit_pr_review,
|
||||
list_pr_review_comments,
|
||||
],
|
||||
backend=sandbox_backend,
|
||||
middleware=[
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
from .commit_and_open_pr import commit_and_open_pr
|
||||
from .fetch_url import fetch_url
|
||||
from .github_comment import github_comment
|
||||
from .github_review import (
|
||||
create_pr_review,
|
||||
dismiss_pr_review,
|
||||
get_pr_review,
|
||||
list_pr_review_comments,
|
||||
list_pr_reviews,
|
||||
submit_pr_review,
|
||||
update_pr_review,
|
||||
)
|
||||
from .http_request import http_request
|
||||
from .linear_comment import linear_comment
|
||||
from .linear_create_issue import linear_create_issue
|
||||
|
|
@ -13,10 +22,15 @@ from .slack_thread_reply import slack_thread_reply
|
|||
|
||||
__all__ = [
|
||||
"commit_and_open_pr",
|
||||
"create_pr_review",
|
||||
"dismiss_pr_review",
|
||||
"fetch_url",
|
||||
"get_pr_review",
|
||||
"github_comment",
|
||||
"http_request",
|
||||
"linear_comment",
|
||||
"list_pr_review_comments",
|
||||
"list_pr_reviews",
|
||||
"linear_create_issue",
|
||||
"linear_delete_issue",
|
||||
"linear_get_issue",
|
||||
|
|
@ -24,4 +38,6 @@ __all__ = [
|
|||
"linear_list_teams",
|
||||
"linear_update_issue",
|
||||
"slack_thread_reply",
|
||||
"submit_pr_review",
|
||||
"update_pr_review",
|
||||
]
|
||||
|
|
|
|||
300
agent/tools/github_review.py
Normal file
300
agent/tools/github_review.py
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from langgraph.config import get_config
|
||||
|
||||
from ..utils.github_app import get_github_app_installation_token
|
||||
|
||||
GITHUB_API_BASE = "https://api.github.com"
|
||||
|
||||
|
||||
def _get_repo_config() -> dict[str, str]:
|
||||
config = get_config()
|
||||
return config.get("configurable", {}).get("repo", {})
|
||||
|
||||
|
||||
def _github_headers(token: str) -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
}
|
||||
|
||||
|
||||
async def _get_token() -> str | None:
|
||||
return await get_github_app_installation_token()
|
||||
|
||||
|
||||
def _repo_url(repo_config: dict[str, str]) -> str:
|
||||
owner = repo_config.get("owner", "")
|
||||
name = repo_config.get("name", "")
|
||||
return f"{GITHUB_API_BASE}/repos/{owner}/{name}"
|
||||
|
||||
|
||||
def list_pr_reviews(pull_number: int) -> dict[str, Any]:
|
||||
"""List all reviews on a pull request."""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews"
|
||||
|
||||
async def _fetch() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=_github_headers(token))
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "reviews": response.json()}
|
||||
|
||||
return asyncio.run(_fetch())
|
||||
|
||||
|
||||
def get_pr_review(pull_number: int, review_id: int) -> dict[str, Any]:
|
||||
"""Get a specific review on a pull request by review ID."""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews/{review_id}"
|
||||
|
||||
async def _fetch() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=_github_headers(token))
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "review": response.json()}
|
||||
|
||||
return asyncio.run(_fetch())
|
||||
|
||||
|
||||
def create_pr_review(
|
||||
pull_number: int,
|
||||
body: str | None = None,
|
||||
event: str = "COMMENT",
|
||||
comments: list[dict[str, Any]] | None = None,
|
||||
commit_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a review on a pull request.
|
||||
|
||||
Args:
|
||||
pull_number: The PR number to review.
|
||||
body: The review body text (required for APPROVE/REQUEST_CHANGES, optional for COMMENT).
|
||||
event: The review action - one of APPROVE, REQUEST_CHANGES, or COMMENT.
|
||||
comments: Optional list of review comments. Each comment dict should have:
|
||||
- path (str): The relative file path to comment on.
|
||||
- body (str): The comment text.
|
||||
- line (int, optional): The line number in the diff to comment on.
|
||||
- side (str, optional): Which side of the diff to comment on (LEFT or RIGHT).
|
||||
- start_line (int, optional): For multi-line comments, the start line.
|
||||
- start_side (str, optional): For multi-line comments, the start side.
|
||||
commit_id: Optional SHA of the commit to review. Defaults to the latest commit.
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and the created review data.
|
||||
"""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews"
|
||||
payload: dict[str, Any] = {"event": event}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
if comments:
|
||||
payload["comments"] = comments
|
||||
if commit_id:
|
||||
payload["commit_id"] = commit_id
|
||||
|
||||
async def _create() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=_github_headers(token), json=payload)
|
||||
if response.status_code not in (200, 201):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "review": response.json()}
|
||||
|
||||
return asyncio.run(_create())
|
||||
|
||||
|
||||
def update_pr_review(
|
||||
pull_number: int,
|
||||
review_id: int,
|
||||
body: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Update the body of an existing review on a pull request.
|
||||
|
||||
Args:
|
||||
pull_number: The PR number.
|
||||
review_id: The ID of the review to update.
|
||||
body: The new review body text.
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and the updated review data.
|
||||
"""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews/{review_id}"
|
||||
|
||||
async def _update() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(url, headers=_github_headers(token), json={"body": body})
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "review": response.json()}
|
||||
|
||||
return asyncio.run(_update())
|
||||
|
||||
|
||||
def dismiss_pr_review(
|
||||
pull_number: int,
|
||||
review_id: int,
|
||||
message: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Dismiss a review on a pull request.
|
||||
|
||||
Args:
|
||||
pull_number: The PR number.
|
||||
review_id: The ID of the review to dismiss.
|
||||
message: A message explaining why the review is being dismissed.
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and the dismissed review data.
|
||||
"""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews/{review_id}/dismissals"
|
||||
|
||||
async def _dismiss() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.put(
|
||||
url, headers=_github_headers(token), json={"message": message}
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "review": response.json()}
|
||||
|
||||
return asyncio.run(_dismiss())
|
||||
|
||||
|
||||
def submit_pr_review(
|
||||
pull_number: int,
|
||||
review_id: int,
|
||||
body: str | None = None,
|
||||
event: str = "COMMENT",
|
||||
) -> dict[str, Any]:
|
||||
"""Submit a pending review on a pull request.
|
||||
|
||||
Use this if a review was created without an event (pending state) and needs to be submitted.
|
||||
|
||||
Args:
|
||||
pull_number: The PR number.
|
||||
review_id: The ID of the pending review to submit.
|
||||
body: Optional body text for the review submission.
|
||||
event: The review action - one of APPROVE, REQUEST_CHANGES, or COMMENT.
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and the submitted review data.
|
||||
"""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews/{review_id}/events"
|
||||
payload: dict[str, Any] = {"event": event}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
|
||||
async def _submit() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=_github_headers(token), json=payload)
|
||||
if response.status_code not in (200, 201):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "review": response.json()}
|
||||
|
||||
return asyncio.run(_submit())
|
||||
|
||||
|
||||
def list_pr_review_comments(
|
||||
pull_number: int,
|
||||
review_id: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""List comments on a pull request review.
|
||||
|
||||
Args:
|
||||
pull_number: The PR number.
|
||||
review_id: If provided, list comments for a specific review.
|
||||
If not provided, list all review comments on the PR.
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and the list of review comments.
|
||||
"""
|
||||
repo_config = _get_repo_config()
|
||||
if not repo_config:
|
||||
return {"success": False, "error": "No repo config found"}
|
||||
|
||||
token = asyncio.run(_get_token())
|
||||
if not token:
|
||||
return {"success": False, "error": "Failed to get GitHub App installation token"}
|
||||
|
||||
if review_id is not None:
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/reviews/{review_id}/comments"
|
||||
else:
|
||||
url = f"{_repo_url(repo_config)}/pulls/{pull_number}/comments"
|
||||
|
||||
async def _fetch() -> dict[str, Any]:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=_github_headers(token))
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"GitHub API returned {response.status_code}: {response.text}",
|
||||
}
|
||||
return {"success": True, "comments": response.json()}
|
||||
|
||||
return asyncio.run(_fetch())
|
||||
Loading…
Add table
Reference in a new issue