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