mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-01 08:33:14 +00:00
* 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>
150 lines
5.1 KiB
Python
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
|