pr-reviewer/app/config.py
Adam Moussa 89494c1710
Ground reviews in the engineering-handbook
Feed the reviewer a distilled digest of the Sea Haven engineering-handbook
so findings reflect our naming, commit, PR, secrets, and IaC conventions
instead of generic code-review judgment.

A new handbook module keeps an app-managed shallow clone of the (private)
handbook, distills the review-relevant pages into a compact conventions
checklist via the Fireworks model once a day, caches it under ~/.cache,
and hands it to the reviewer to inject into every review's system prompt.
The refresh runs in-process at the start of each worker cycle; failures
keep the last good digest and back off, so a handbook outage never blocks
reviews. Set HANDBOOK_ENABLED=false to disable.

Extract a shared fireworks_complete helper used by both the reviewer and
the distiller, so the handbook provider needs no reviewer reference and
the guidance callable is set once at construction. Clone auth uses a
Basic http.extraHeader (GitHub git-over-HTTPS rejects Bearer), and the
distiller wraps its answer in delimiters to strip a reasoning model's
chain-of-thought preamble. Adds GET /api/handbook and a header status
line. Stdlib-only, no new dependencies.
2026-07-01 17:08:21 -04:00

101 lines
4.2 KiB
Python

"""Configuration loaded from environment / .env.
Secrets never reach the browser. The frontend talks only to this local
backend; the backend holds the GitHub PAT and Fireworks key.
"""
import os
from functools import lru_cache
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
_REPO_ROOT = Path(__file__).resolve().parent.parent
_CACHE_HOME = Path.home() / ".cache" / "pr-reviewer"
class Config:
# --- GitHub ---
# Either provide GITHUB_TOKEN (a PAT) here, or leave it blank to fall
# back to `gh auth token` from your local gh CLI (see github_client.py).
GITHUB_TOKEN: str = os.getenv("GITHUB_TOKEN", "").strip()
GITHUB_ORG: str = os.getenv("GITHUB_ORG", "Sea-Haven-Industries").strip()
# The search filter that defines the review queue.
PR_SEARCH_FILTER: str = os.getenv(
"PR_SEARCH_FILTER",
"is:pr state:open archived:false sort:updated-desc org:Sea-Haven-Industries",
).strip()
# Cap how many PRs we pull per refresh. GitHub search returns at most 100
# per page; PRs beyond that are not paginated (fine at current org volume).
MAX_PRS: int = int(os.getenv("MAX_PRS", "100"))
# Skip diffs larger than this many bytes (keeps token cost sane).
MAX_DIFF_BYTES: int = int(os.getenv("MAX_DIFF_BYTES", "120000"))
# --- Fireworks (OpenAI-compatible endpoint) ---
FIREWORKS_API_KEY: str = os.getenv("FIREWORKS_API_KEY", "").strip()
FIREWORKS_BASE_URL: str = os.getenv(
"FIREWORKS_BASE_URL", "https://api.fireworks.ai/inference/v1"
).strip()
# Swap this to any Fireworks model id you like. Coding-strong defaults:
# accounts/fireworks/models/deepseek-v4-pro
# accounts/fireworks/models/kimi-k2p6
FIREWORKS_MODEL: str = os.getenv(
"FIREWORKS_MODEL", "accounts/fireworks/models/deepseek-v4-pro"
).strip()
FIREWORKS_TEMPERATURE: float = float(os.getenv("FIREWORKS_TEMPERATURE", "0.2"))
FIREWORKS_MAX_TOKENS: int = int(os.getenv("FIREWORKS_MAX_TOKENS", "4000"))
# --- Review behavior ---
# If a PR author's login is in this list (case-insensitive), the review
# body will @-mention them. Handles the @openswe bot case.
MENTION_AUTHORS: list[str] = [
a.strip().lower()
for a in os.getenv("MENTION_AUTHORS", "openswe").split(",")
if a.strip()
]
HOST: str = os.getenv("HOST", "127.0.0.1")
PORT: int = int(os.getenv("PORT", "8765"))
# --- Background auto-review worker ---
# Seconds between poll cycles. The worker fetches the queue, then pre-reviews
# any new or changed non-draft PR so results are ready before you open them.
POLL_INTERVAL: int = int(os.getenv("POLL_INTERVAL", "300"))
# How many PRs to review in parallel per cycle. Kept low for a single-user
# tool to stay well under Fireworks rate limits.
WORKER_CONCURRENCY: int = int(os.getenv("WORKER_CONCURRENCY", "2"))
# Give up auto-retrying a PR that keeps failing until its diff changes.
MAX_REVIEW_ATTEMPTS: int = int(os.getenv("MAX_REVIEW_ATTEMPTS", "3"))
# SQLite cache of pre-computed reviews. Absolute path resolved from the repo
# root so it lands in the same place regardless of the working directory.
CACHE_DB: str = os.getenv("CACHE_DB", str(_REPO_ROOT / "pr_cache.db"))
# --- Engineering-handbook grounding ---
# When enabled, the reviewer is fed a distilled digest of the Sea Haven
# engineering-handbook conventions, refreshed daily from an app-managed clone.
HANDBOOK_ENABLED: bool = os.getenv("HANDBOOK_ENABLED", "true").lower() in (
"1",
"true",
"yes",
"on",
)
HANDBOOK_REPO_URL: str = os.getenv(
"HANDBOOK_REPO_URL",
"https://github.com/Sea-Haven-Industries/engineering-handbook.git",
)
# App-managed clone + digest cache, kept outside the repo (in ~/.cache).
HANDBOOK_CACHE_DIR: str = os.getenv(
"HANDBOOK_CACHE_DIR", str(_CACHE_HOME / "handbook")
)
HANDBOOK_DIGEST_PATH: str = os.getenv(
"HANDBOOK_DIGEST_PATH", str(_CACHE_HOME / "handbook_digest.json")
)
# Re-pull and re-distill the handbook at most this often.
HANDBOOK_REFRESH_HOURS: int = int(os.getenv("HANDBOOK_REFRESH_HOURS", "24"))
@lru_cache
def get_config() -> Config:
return Config()