open-swe/agent/reviewer_reconcile.py
Johannes du Plessis 5702a9d452
feat: reconcile reviewer comment lifecycle (#1332)
* feat: reconcile reviewer comment lifecycle

Track GitHub review threads for reviewer findings so re-reviews can resolve or reply to existing comments, and collect thumbs feedback on new review comments in LangSmith.

* fix: clarify reviewer comment lifecycle

* chore: apply reviewer formatting

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-26 16:24:34 -07:00

150 lines
5.1 KiB
Python

from __future__ import annotations
from typing import Any
from .reviewer_findings import Finding, list_findings, replace_findings
ReviewThread = dict[str, Any]
def _human_replies_after_bot_comment(
review_thread: ReviewThread,
*,
bot_comment_id: int,
) -> list[ReviewThread]:
comments = review_thread.get("comments")
if not isinstance(comments, list):
return []
seen_bot_comment = False
replies: list[ReviewThread] = []
for comment in comments:
if not isinstance(comment, dict):
continue
comment_id = comment.get("id")
if comment_id == bot_comment_id:
seen_bot_comment = True
continue
if not seen_bot_comment:
continue
author = comment.get("author")
if author == "open-swe[bot]":
continue
replies.append(comment)
return replies
def _index_review_threads(
review_threads: list[ReviewThread],
) -> tuple[dict[str, ReviewThread], dict[int, ReviewThread]]:
by_thread_id = {
thread_id: review_thread
for review_thread in review_threads
if isinstance(thread_id := review_thread.get("id"), str) and thread_id
}
by_comment_id: dict[int, ReviewThread] = {}
for review_thread in review_threads:
comments = review_thread.get("comments")
if not isinstance(comments, list):
continue
for comment in comments:
if not isinstance(comment, dict):
continue
comment_id = comment.get("id")
if isinstance(comment_id, int):
by_comment_id[comment_id] = review_thread
return by_thread_id, by_comment_id
def _find_review_thread_for_finding(
finding: Finding,
*,
by_thread_id: dict[str, ReviewThread],
by_comment_id: dict[int, ReviewThread],
) -> ReviewThread | None:
github_thread_id = finding.get("github_review_thread_id")
if isinstance(github_thread_id, str) and github_thread_id:
review_thread = by_thread_id.get(github_thread_id)
if review_thread is not None:
return review_thread
github_comment_id = finding.get("github_review_comment_id")
if isinstance(github_comment_id, int):
return by_comment_id.get(github_comment_id)
return None
def _sync_thread_status(finding: Finding, review_thread: ReviewThread) -> bool:
updated = False
github_thread_id = finding.get("github_review_thread_id")
if not isinstance(github_thread_id, str) or not github_thread_id:
new_thread_id = review_thread.get("id")
if isinstance(new_thread_id, str) and new_thread_id:
finding["github_review_thread_id"] = new_thread_id
updated = True
if review_thread.get("is_resolved") or review_thread.get("is_outdated"):
if finding.get("status") == "open":
finding["status"] = "resolved"
finding["last_reconciliation_note"] = "GitHub thread is resolved or outdated."
updated = True
if review_thread.get("is_resolved") and not finding.get("github_thread_resolved"):
finding["github_thread_resolved"] = True
updated = True
return updated
def _sync_latest_human_reply(finding: Finding, review_thread: ReviewThread) -> bool:
github_comment_id = finding.get("github_review_comment_id")
if not isinstance(github_comment_id, int):
return False
replies = _human_replies_after_bot_comment(review_thread, bot_comment_id=github_comment_id)
if not replies:
return False
latest = replies[-1]
body = latest.get("body") if isinstance(latest.get("body"), str) else ""
if len(body) > 1000:
body = body[:1000] + "\n...[truncated]"
created_at = latest.get("created_at") if isinstance(latest.get("created_at"), str) else ""
if finding.get("last_human_reply_at") == created_at:
return False
author = latest.get("author") if isinstance(latest.get("author"), str) else ""
finding["last_human_reply_at"] = created_at
finding["last_human_reply_author"] = author
finding["last_human_reply_body"] = body
finding["last_reconciliation_note"] = (
"Human replied to this review thread; reassess before taking action."
)
return True
async def reconcile_findings_with_review_threads(
reviewer_thread_id: str,
review_threads: list[ReviewThread],
) -> list[Finding]:
"""Sync tracked Open SWE findings with the current GitHub review-thread state."""
findings = await list_findings(reviewer_thread_id)
if not findings:
return findings
by_thread_id, by_comment_id = _index_review_threads(review_threads)
updated = False
for finding in findings:
review_thread = _find_review_thread_for_finding(
finding,
by_thread_id=by_thread_id,
by_comment_id=by_comment_id,
)
if review_thread is None:
continue
updated = _sync_thread_status(finding, review_thread) or updated
updated = _sync_latest_human_reply(finding, review_thread) or updated
if updated:
await replace_findings(reviewer_thread_id, findings)
return findings