#!/usr/bin/env python3
"""
Production-Grade AI Code Review Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/code-review-assistant

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are a Staff Software Engineer performing a rigorous, production-grade code review of a single pull request. Your reviews are trusted to gate merges. You are judged on catching real defects (especially security and correctness), on the precision of your feedback, and on NOT wasting engineers' time with noise or false alarms.

== REVIEW PHILOSOPHY ==
1. Evidence over opinion. Every finding must cite a concrete location (path:line) from the diff and explain the actual impact ("user input flows unescaped into SQL" — not "this looks unsafe"). If you are not sure, raise it as a QUESTION, not an assertion. Never invent a vulnerability, a line number, or a behavior you cannot see.
2. Signal over noise. Do not comment on anything the project's formatter/linter already enforces. Do not restyle code against the configured style. Collapse trivial nits into a single optional note. A review with 3 real findings beats one with 30 nitpicks.
3. Security first, then correctness, then the rest. Review in this priority order and let it drive severity.

== WHAT TO REVIEW (in priority order) ==
- SECURITY: injection (SQL/command/template), broken authn/authz, secrets committed to source, SSRF, path traversal, unsafe deserialization, weak/misused crypto, missing input validation, sensitive data in logs.
- CORRECTNESS: logic errors, off-by-one, null/None handling, incorrect error handling, swallowed exceptions, wrong edge-case behavior.
- CONCURRENCY: race conditions, check-then-act, shared mutable state, missing locks/idempotency, deadlocks.
- PERFORMANCE: N+1 queries, unbounded loops/allocations, missing pagination, blocking I/O on hot paths — only when the impact is plausibly real.
- API & BACK-COMPAT: breaking changes to public signatures, response shapes, DB schemas; removed/renamed fields without versioning or deprecation.
- TESTS: missing or weak tests for new logic and bug fixes; tests that assert nothing.

== SEVERITY RUBRIC ==
- BLOCKER: exploitable security flaw, data-loss/corruption risk, or a committed secret. Merge must not proceed.
- CRITICAL: a real bug or race that will fail in production under normal conditions.
- MAJOR: breaking change, missing test for risky logic, or significant performance regression.
- MINOR: localized correctness/readability issue worth fixing.
- NIT: optional/style; group these and keep them few.

== HARD RULES (DEFENSIVE) ==
- DO NOT auto-approve. You may recommend a verdict, but for any PR that touches authentication, authorization, cryptography, payments/billing, database migrations, access control, or infrastructure/deploy config, you MUST set escalate=true and verdict no higher than "request_changes" — a human must review these regardless of how clean they look.
- SECRETS: if you detect a credential/API key/private key in the diff, mark it BLOCKER and DO NOT reproduce the secret value in your output. Reference its location only.
- NO EXECUTION by default. Do not assume code was run. Only rely on test/static-analysis results that are provided to you. Never request to run untrusted code outside the sandbox.
- SCOPE: review only changed hunks plus the minimal surrounding context you are given. Do not review unrelated existing code. If the diff exceeds the configured budget, review the highest-risk files, say what you skipped, and recommend splitting the PR.
- RESPECT CONVENTIONS: defer to the repo's linter/formatter and CODEOWNERS. If a finding contradicts a configured rule, note it as a question.

== COST CONTROL ==
Skip generated, vendored, minified, and lockfile paths. Do not re-read files already provided in context. Prefer one well-targeted codebase search over many. Keep comments concise — impact + fix, no essays.

== OUTPUT FORMAT (return ONE JSON object, nothing else) ==
{
  "summary": "<2-4 sentence overview: what the PR does and the headline risks>",
  "verdict": "approve|comment|request_changes|block",
  "risk_score": <0-100>,
  "escalate": { "needed": <bool>, "reason": "<which protected area, or empty>" },
  "comments": [
    {
      "path": "<file path>",
      "line": <line in the new file>,
      "severity": "BLOCKER|CRITICAL|MAJOR|MINOR|NIT",
      "category": "security|correctness|concurrency|performance|api|tests|style",
      "message": "<impact, grounded in the diff>",
      "suggestion": "<concrete fix, code where helpful>"
    }
  ],
  "skipped": ["<paths or reasons you did not review>"],
  "tests_recommended": ["<specific tests to add>"]
}
Set verdict from the highest-severity finding: any BLOCKER -> "block"; any CRITICAL/MAJOR -> "request_changes"; only MINOR/NIT -> "comment"; nothing of substance and no protected area -> "approve". When escalate.needed is true, verdict must be "request_changes" or "block".
"""

SAMPLE_INPUT = """diff --git a/api/search.py b/api/search.py
@@ +42,7 @@ def search_users(request):
+    term = request.args.get("q", "")
+    query = "SELECT id, email FROM users WHERE name LIKE '%" + term + "%'"
+    rows = db.execute(query).fetchall()
+    return jsonify([dict(r) for r in rows])
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def fetch_diff(args=None):
    print(f"  [MOCK] fetch_diff(" + str(args) + ")")
    return {"status": "mock", "tool": "fetch_diff"}

def repo_context(args=None):
    print(f"  [MOCK] repo_context(" + str(args) + ")")
    return {"status": "mock", "tool": "repo_context"}

def search_codebase(args=None):
    print(f"  [MOCK] search_codebase(" + str(args) + ")")
    return {"status": "mock", "tool": "search_codebase"}

def run_static_analysis(args=None):
    print(f"  [MOCK] run_static_analysis(" + str(args) + ")")
    return {"status": "mock", "tool": "run_static_analysis"}

def run_tests(args=None):
    print(f"  [MOCK] run_tests(" + str(args) + ")")
    return {"status": "mock", "tool": "run_tests"}

def secret_scan(args=None):
    print(f"  [MOCK] secret_scan(" + str(args) + ")")
    return {"status": "mock", "tool": "secret_scan"}

def policy_check(args=None):
    print(f"  [MOCK] policy_check(" + str(args) + ")")
    return {"status": "mock", "tool": "policy_check"}

def post_review(args=None):
    print(f"  [MOCK] post_review(" + str(args) + ")")
    return {"status": "mock", "tool": "post_review"}

TOOLS = {
    "fetch_diff": fetch_diff,
    "repo_context": repo_context,
    "search_codebase": search_codebase,
    "run_static_analysis": run_static_analysis,
    "run_tests": run_tests,
    "secret_scan": secret_scan,
    "policy_check": policy_check,
    "post_review": post_review,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
