Files
ci-ai/scripts/ai_review.py

107 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Feature A — KI-PR-Review + Summary.
Holt den PR-Diff über die Gitea-API, lässt ihn vom konfigurierten LLM
zusammenfassen + reviewen und postet das Ergebnis als EINEN idempotenten
Kommentar (Marker -> editiert sich bei jedem Push, statt zu spammen).
Erwartete Env-Vars: siehe llm.py + gitea_api.py, plus die Actions-Kontext-
Variablen (GITHUB_REPOSITORY, GITHUB_EVENT_PATH).
"""
import sys
import context
import gitea_api
import llm
MARKER = "<!-- ci-ai:review -->"
DIFF_CAP = 40000 # Zeichen — darüber wird der Diff gekürzt (Token-/Kostenschutz)
# Dateien, die für ein Review wenig Sinn ergeben (Lockfiles, Binär, generiert).
SKIP_TOKENS = (
".lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
".min.js", ".min.css", ".map",
".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".woff", ".woff2", ".ttf",
"/vendor/", "/node_modules/", "/dist/", "/build/",
)
SYSTEM = (
"Du bist ein erfahrener, pragmatischer Code-Reviewer. Antworte auf Deutsch, "
"knapp und konkret. Erfinde nichts, was nicht im Diff steht."
)
PROMPT = """\
Reviewe den folgenden Pull-Request.
PR-Titel: {title}
PR-Beschreibung: {body}
Gib genau diese zwei Abschnitte aus (Markdown):
### Zusammenfassung
24 Sätze: Was ändert dieser PR und warum.
### Review
Stichpunkte zu: möglichen Bugs, Sicherheits-/Edge-Cases, fehlenden Tests,
Stil/Lesbarkeit. Nur echte, im Diff belegbare Punkte — wenn alles gut aussieht,
sag das ehrlich. Markiere ernste Probleme mit ⚠️.
--- DIFF ---
{diff}
"""
def filter_diff(diff: str) -> str:
"""Entfernt uninteressante Dateien (per `diff --git`-Sektion)."""
if not diff:
return ""
sections = diff.split("diff --git ")
kept = []
for sec in sections[1:]: # sections[0] ist der Vorspann (leer)
header = sec.split("\n", 1)[0].lower()
if any(tok in header for tok in SKIP_TOKENS):
continue
kept.append("diff --git " + sec)
return "".join(kept).strip()
def main() -> int:
owner, repo = context.repo_slug()
pr = context.pull_request()
if not pr or not pr["index"]:
print("[ai_review] Kein Pull-Request im Event — übersprungen.")
return 0
if pr["author"].lower().endswith("-bot") or pr["author"].lower() == "ci-ai":
print("[ai_review] PR vom Bot selbst — übersprungen.")
return 0
ok, why = llm.configured()
if not ok:
print(f"[ai_review] KI inaktiv ({why}) — Schritt sauber übersprungen, kein Fehler.")
return 0
diff = filter_diff(gitea_api.get_pr_diff(owner, repo, pr["index"]))
if not diff:
print("[ai_review] Leerer/gefilterter Diff — nichts zu reviewen.")
return 0
truncated = False
if len(diff) > DIFF_CAP:
diff = diff[:DIFF_CAP]
truncated = True
answer = llm.complete(
SYSTEM,
PROMPT.format(title=pr["title"], body=pr["body"][:1000], diff=diff),
max_tokens=1800,
)
note = "\n\n_⚠ Diff war groß und wurde gekürzt — Review nur über den Anfang._" if truncated else ""
body = f"## 🤖 KI-Review\n\n{answer}{note}"
cid = gitea_api.upsert_comment(owner, repo, pr["index"], MARKER, body)
print(f"[ai_review] Kommentar gepostet/aktualisiert (id={cid}).")
return 0
if __name__ == "__main__":
sys.exit(main())