pr-reviewer/app/main.py

146 lines
3.9 KiB
Python
Raw Normal View History

"""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()