mirror of
https://github.com/Sea-Haven-Industries/pr-reviewer.git
synced 2026-09-30 10:23:15 +00:00
A single-user local dashboard that pulls open PRs from the Sea-Haven-Industries org, reviews each with a Fireworks model in the BLOCK/FIX/NIT/QUESTION format, and posts the review to GitHub as the token owner. Runs only on localhost; secrets stay in a gitignored .env and never reach the browser. Structured as an app/ package plus a static/ frontend so the module imports and static mount resolve. HTTP uses httpx2 (the runtime lib starlette's TestClient now prefers), pinned in requirements.txt. Includes a stdlib-only pytest suite (network mocked, no extra test deps so CI needs only pytest) with a skip-by-default live Fireworks test, and CI wired to the org ci-python-app reusable workflow to lint app and tests and run the mocked suite fully offline. Dependabot covers pip and github-actions.
145 lines
3.9 KiB
Python
145 lines
3.9 KiB
Python
"""FastAPI backend for the local PR review dashboard.
|
|
|
|
Endpoints:
|
|
GET / -> serves the dashboard
|
|
GET /api/prs -> list PRs matching the filter (no review yet)
|
|
POST /api/review -> run a Fireworks review for one PR
|
|
POST /api/revise -> re-run review with the user's revision notes
|
|
POST /api/post -> submit the review to GitHub (side-effectful)
|
|
|
|
In-memory store only; state resets on restart. This is a single-user local tool.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
from .config import get_config
|
|
from .github_client import GitHubClient, GitHubError
|
|
from .reviewer import Reviewer
|
|
|
|
cfg = get_config()
|
|
app = FastAPI(title="PR Review Dashboard")
|
|
|
|
STATIC_DIR = Path(__file__).parent.parent / "static"
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
|
|
# lazy singletons so a missing token doesn't crash import
|
|
_gh: GitHubClient | None = None
|
|
_reviewer: Reviewer | None = None
|
|
|
|
|
|
def gh() -> GitHubClient:
|
|
global _gh
|
|
if _gh is None:
|
|
_gh = GitHubClient(cfg)
|
|
return _gh
|
|
|
|
|
|
def reviewer() -> Reviewer:
|
|
global _reviewer
|
|
if _reviewer is None:
|
|
_reviewer = Reviewer(cfg)
|
|
return _reviewer
|
|
|
|
|
|
@app.get("/")
|
|
def index() -> FileResponse:
|
|
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
|
|
@app.get("/api/config")
|
|
def api_config() -> dict[str, Any]:
|
|
return {
|
|
"org": cfg.GITHUB_ORG,
|
|
"filter": cfg.PR_SEARCH_FILTER,
|
|
"model": cfg.FIREWORKS_MODEL,
|
|
"mention_authors": cfg.MENTION_AUTHORS,
|
|
}
|
|
|
|
|
|
@app.get("/api/prs")
|
|
def api_prs() -> dict[str, Any]:
|
|
try:
|
|
prs = gh().search_prs()
|
|
return {"prs": prs, "me": gh().whoami()}
|
|
except GitHubError as e:
|
|
raise HTTPException(400, str(e))
|
|
|
|
|
|
class ReviewReq(BaseModel):
|
|
owner: str
|
|
repo: str
|
|
number: int
|
|
title: str = ""
|
|
author: str = ""
|
|
body: str = ""
|
|
|
|
|
|
@app.post("/api/review")
|
|
def api_review(req: ReviewReq) -> dict[str, Any]:
|
|
try:
|
|
diff = gh().pr_diff(req.owner, req.repo, req.number)
|
|
pr = req.model_dump()
|
|
result = reviewer().review(pr, diff)
|
|
return {"review": result}
|
|
except GitHubError as e:
|
|
raise HTTPException(400, str(e))
|
|
except Exception as e: # surface Fireworks/parse errors to the UI
|
|
raise HTTPException(500, f"Review failed: {e}")
|
|
|
|
|
|
class ReviseReq(ReviewReq):
|
|
notes: str # the user's requested changes to the review
|
|
|
|
|
|
@app.post("/api/revise")
|
|
def api_revise(req: ReviseReq) -> dict[str, Any]:
|
|
try:
|
|
diff = gh().pr_diff(req.owner, req.repo, req.number)
|
|
pr = req.model_dump()
|
|
# Fold the user's revision notes into the diff context as a trusted
|
|
# reviewer instruction (distinct from the untrusted diff).
|
|
annotated = (
|
|
f"{diff}\n\n--- REVIEWER REVISION NOTES (trusted, from the human "
|
|
f"reviewer, not from the PR) ---\n{req.notes}"
|
|
)
|
|
result = reviewer().review(pr, annotated)
|
|
return {"review": result}
|
|
except GitHubError as e:
|
|
raise HTTPException(400, str(e))
|
|
except Exception as e: # surface Fireworks/parse errors to the UI
|
|
raise HTTPException(500, f"Revision failed: {e}")
|
|
|
|
|
|
class PostReq(BaseModel):
|
|
owner: str
|
|
repo: str
|
|
number: int
|
|
body: str
|
|
event: str # COMMENT | APPROVE | REQUEST_CHANGES
|
|
|
|
|
|
@app.post("/api/post")
|
|
def api_post(req: PostReq) -> dict[str, Any]:
|
|
try:
|
|
res = gh().submit_review(req.owner, req.repo, req.number, req.body, req.event)
|
|
return {"ok": True, "html_url": res.get("html_url", "")}
|
|
except GitHubError as e:
|
|
raise HTTPException(400, str(e))
|
|
|
|
|
|
def main() -> None:
|
|
import uvicorn
|
|
|
|
uvicorn.run(app, host=cfg.HOST, port=cfg.PORT)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|