Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions plugins/pr-review/scripts/agent_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,44 @@ def _get_bool_env(name: str, default: bool = False) -> bool:
return value.strip().lower() in {"1", "true", "yes", "on"}


def get_session_url() -> str | None:
"""Return a link to the current execution session.

Detects the environment automatically:

- **GitHub Actions**: constructs the workflow run URL from standard
``GITHUB_*`` environment variables.
- **OpenHands Cloud Automation**: constructs the conversation URL from
``SANDBOX_ID`` and ``OPENHANDS_CLOUD_API_URL``, which are injected by
the cloud automation backend.
- **Explicit override**: if ``REVIEW_RUN_URL`` is set it takes precedence
over auto-detection (preserves backward-compatibility for callers that
already supply the URL).

Returns:
URL string, or ``None`` if the environment cannot be determined.
"""
explicit = os.getenv("REVIEW_RUN_URL", "").strip()
if explicit:
return explicit

# GitHub Actions
if os.getenv("GITHUB_ACTIONS") == "true":
server = os.getenv("GITHUB_SERVER_URL", "https://github.com").rstrip("/")
repo = os.getenv("GITHUB_REPOSITORY", "")
run_id = os.getenv("GITHUB_RUN_ID", "")
if repo and run_id:
return f"{server}/{repo}/actions/runs/{run_id}"

# OpenHands Cloud Automation
sandbox_id = os.getenv("SANDBOX_ID", "").strip()
cloud_url = os.getenv("OPENHANDS_CLOUD_API_URL", "").rstrip("/")
if sandbox_id and cloud_url:
return f"{cloud_url}/conversations/{sandbox_id}"

return None


def _call_github_api(
url: str,
method: str = "GET",
Expand Down Expand Up @@ -865,6 +903,62 @@ def get_head_commit_sha(repo_dir: Path | None = None) -> str:
return run_git_command(["git", "rev-parse", "HEAD"], repo_dir).strip()


def post_progress_comment(
repo_name: str, pr_number: str, session_url: str | None
) -> int | None:
"""Post a 'review in progress' comment on the PR before the agent starts.

Args:
repo_name: Repository in ``owner/repo`` format.
pr_number: Pull request number.
session_url: Link to the execution session, or ``None``.

Returns:
GitHub comment ID on success, ``None`` on failure (non-fatal).
"""
parts = ["🔍 **Review in progress…**"]
if session_url:
parts.append(f"[View session]({session_url})")
body = " · ".join(parts)
try:
result = _call_github_api(
f"/repos/{repo_name}/issues/{pr_number}/comments",
method="POST",
data={"body": body},
)
comment_id: int = result["id"]
logger.info("Posted progress comment id=%s", comment_id)
return comment_id
except Exception as exc:
logger.warning("Failed to post progress comment: %s", exc)
return None


def update_progress_comment(
repo_name: str, comment_id: int, session_url: str | None
) -> None:
"""Update the progress comment to 'review complete' after the agent finishes.

Args:
repo_name: Repository in ``owner/repo`` format.
comment_id: ID returned by :func:`post_progress_comment`.
session_url: Link to the execution session, or ``None``.
"""
parts = ["✅ **Review complete.**"]
if session_url:
parts.append(f"[View session]({session_url})")
body = " · ".join(parts)
try:
_call_github_api(
f"/repos/{repo_name}/issues/comments/{comment_id}",
method="PATCH",
data={"body": body},
)
logger.info("Updated progress comment id=%s", comment_id)
except Exception as exc:
logger.warning("Failed to update progress comment id=%s: %s", comment_id, exc)


def validate_environment() -> dict[str, Any]:
"""Validate required environment variables and return config.

Expand Down Expand Up @@ -928,6 +1022,7 @@ def validate_environment() -> dict[str, Any]:
"require_evidence": _get_bool_env("REQUIRE_EVIDENCE"),
"collect_feedback": _get_bool_env("COLLECT_FEEDBACK"),
"review_run_url": os.getenv("REVIEW_RUN_URL", ""),
"session_url": get_session_url(),
"use_sub_agents": use_sub_agents,
"load_public_skills": _get_bool_env("LOAD_PUBLIC_SKILLS", default=True),
"pr_info": {
Expand Down Expand Up @@ -1238,6 +1333,11 @@ def main():
pr_info["number"]
)

session_url = config.get("session_url")
progress_comment_id = post_progress_comment(
pr_info["repo_name"], pr_info["number"], session_url
)

skill_trigger = "/codereview"
logger.info(f"Using skill trigger: {skill_trigger}")

Expand All @@ -1257,6 +1357,7 @@ def main():
collect_feedback=collect_feedback,
review_run_url=config["review_run_url"],
use_sub_agents=use_sub_agents,
session_url=config.get("session_url", ""),
)

secrets = {}
Expand All @@ -1268,6 +1369,11 @@ def main():
conversation = create_conversation(config, secrets)
conversation = run_review(conversation, prompt)

if progress_comment_id:
update_progress_comment(
pr_info["repo_name"], progress_comment_id, session_url
)

log_cost_summary(conversation)
save_trace_context(pr_info, commit_id, config["model"])

Expand Down
27 changes: 26 additions & 1 deletion plugins/pr-review/scripts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@
- If you would otherwise post only inline comments, still include a short top-level review body so this footer has somewhere to live.
"""

_SESSION_LINK_SECTION = """
## Review Session Link

At the very end of the top-level review body, include this attribution line:

_This review was generated by an AI agent (OpenHands). [View session]({session_url})_

Requirements:
- Place this as the last line of the main review body, after all other content.
- Keep the exact markdown formatting, including the italics and hyperlink.
- If you would otherwise post only inline comments with no top-level review body, \
still include a short overall summary so this line has somewhere to live.
"""

PROMPT = """{skill_trigger}
/github-pr-review

Expand All @@ -90,7 +104,7 @@
- **PR Number**: {pr_number}
- **Commit ID**: {commit_id}

{review_context_section}{evidence_requirements_section}{feedback_footer_section}
{review_context_section}{evidence_requirements_section}{feedback_footer_section}{session_link_section}
{files_manifest}
## Patches

Expand Down Expand Up @@ -191,6 +205,7 @@ def format_prompt(
require_evidence: bool = False,
collect_feedback: bool = False,
review_run_url: str = "",
session_url: str = "",
use_sub_agents: bool = False,
) -> str:
"""Format the PR review prompt with all parameters.
Expand All @@ -212,6 +227,9 @@ def format_prompt(
collect_feedback: Whether to instruct the reviewer to append the feedback
footer to the main review body.
review_run_url: Workflow run URL to embed in the feedback footer.
session_url: Link to the current session (GitHub Actions run or
OpenHands Cloud conversation). When provided, the agent
is instructed to include it at the end of the review body.
use_sub_agents: When True, the agent gets the TaskToolSet and decides
at runtime whether to delegate file-level reviews to
sub-agents based on diff size and complexity.
Expand All @@ -238,6 +256,12 @@ def format_prompt(
feedback_comment_marker=FEEDBACK_COMMENT_MARKER,
)

session_link_section = ""
if session_url and not collect_feedback:
# When collect_feedback is True the run URL already appears in the
# feedback footer, so avoid duplicating it.
session_link_section = _SESSION_LINK_SECTION.format(session_url=session_url)

prompt = PROMPT.format(
skill_trigger=skill_trigger,
title=title,
Expand All @@ -250,6 +274,7 @@ def format_prompt(
review_context_section=review_context_section,
evidence_requirements_section=evidence_requirements_section,
feedback_footer_section=feedback_footer_section,
session_link_section=session_link_section,
files_manifest=files_manifest,
diff=diff,
)
Expand Down
Loading