prlens 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prlens-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 lens contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
prlens-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: prlens
3
+ Version: 0.1.0
4
+ Summary: AI-powered GitHub PR code reviewer for teams
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/jodohq/lens
7
+ Project-URL: Issues, https://github.com/jodohq/lens/issues
8
+ Keywords: code-review,github,ai,llm,pull-request
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: PyGithub>=2.1
20
+ Requires-Dist: openai>=1.0
21
+ Requires-Dist: anthropic>=0.25
22
+ Requires-Dist: click>=8.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: python-dotenv>=1.0
25
+ Requires-Dist: rich>=13.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
29
+ Requires-Dist: black>=24.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # lens
33
+
34
+ AI-powered GitHub PR code reviewer for teams. Fetches a pull request, reviews each changed file against your coding guidelines using Claude or GPT-4o, and posts inline comments directly on GitHub.
35
+
36
+ ## Features
37
+
38
+ - Reviews only added/modified files — skips deleted files and binary assets
39
+ - Supports **Anthropic Claude** and **OpenAI GPT-4o** as AI backends
40
+ - Bring your own guidelines via a simple YAML config file
41
+ - Posts inline review comments on the diff using the GitHub Review API
42
+ - Runs as a **CLI tool** locally or as a **GitHub Action** automatically on every PR
43
+ - Prevents duplicate comments across repeated runs
44
+ - Filters known AI false positives (hallucinated suggestions)
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install prlens
50
+ ```
51
+
52
+ ## Quick Start (CLI)
53
+
54
+ ```bash
55
+ export GITHUB_TOKEN=ghp_...
56
+ export ANTHROPIC_API_KEY=sk-ant-...
57
+
58
+ prlens --repo owner/repo --pr 42 --model anthropic
59
+ ```
60
+
61
+ Omit `--pr` to list open PRs and pick one interactively.
62
+
63
+ ## GitHub Action
64
+
65
+ Add this workflow to your repository to automatically review every pull request:
66
+
67
+ ```yaml
68
+ # .github/workflows/code-review.yml
69
+ name: Code Review
70
+
71
+ on:
72
+ pull_request:
73
+ types: [opened, synchronize]
74
+
75
+ jobs:
76
+ review:
77
+ runs-on: ubuntu-latest
78
+ steps:
79
+ - uses: jodohq/prlens/.github/actions/review@main
80
+ with:
81
+ model: anthropic
82
+ github-token: ${{ secrets.GITHUB_TOKEN }}
83
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
84
+ ```
85
+
86
+ ## Configuration
87
+
88
+ Copy `.prlens.example.yml` to `.prlens.yml` in your repository root:
89
+
90
+ ```yaml
91
+ # .prlens.yml
92
+ model: anthropic # anthropic | openai
93
+ max_chars_per_file: 20000
94
+ batch_limit: 60
95
+
96
+ # Path to your team's coding guidelines (relative to repo root)
97
+ # guidelines: ./docs/guidelines.md
98
+
99
+ # Files/directories to skip — fnmatch globs or directory names
100
+ # exclude:
101
+ # - migrations/
102
+ # - "*.min.js"
103
+
104
+ # Set to true to review draft PRs (skipped by default)
105
+ review_draft_prs: false
106
+ ```
107
+
108
+ The `--model` and `--guidelines` CLI flags override the config file. The config file overrides built-in defaults.
109
+
110
+ ## Custom Guidelines
111
+
112
+ Point `guidelines` in `.prlens.yml` to any Markdown file containing your team's coding standards:
113
+
114
+ ```yaml
115
+ guidelines: ./docs/guidelines.md
116
+ ```
117
+
118
+ When not set, built-in generic guidelines are used as a starting point. Copy them from [`prlens/guidelines/`](prlens/guidelines/) and customize.
119
+
120
+ ## Environment Variables
121
+
122
+ | Variable | Required | Description |
123
+ |---|---|---|
124
+ | `GITHUB_TOKEN` | Yes | GitHub personal access token with `pull_requests: write` |
125
+ | `ANTHROPIC_API_KEY` | When using Claude | Anthropic API key |
126
+ | `OPENAI_API_KEY` | When using GPT-4o | OpenAI API key |
127
+
128
+ ## CLI Reference
129
+
130
+ ```
131
+ Usage: prlens [OPTIONS]
132
+
133
+ AI-powered GitHub PR code reviewer.
134
+
135
+ Options:
136
+ --repo TEXT GitHub repository in owner/name format. [required]
137
+ --pr INTEGER Pull request number. Omit to list open PRs interactively.
138
+ --model [anthropic|openai] AI model provider. Overrides config file.
139
+ --config TEXT Path to the configuration file. [default: .prlens.yml]
140
+ -y, --yes Skip confirmation prompts and post comments automatically.
141
+ --guidelines PATH Path to a Markdown guidelines file. Overrides config file.
142
+ -s, --shadow Dry-run mode: print review comments to the terminal without posting to GitHub.
143
+ --help Show this message and exit.
144
+ ```
145
+
146
+ ## Contributing
147
+
148
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up a development environment, run tests, and add new AI providers.
149
+
150
+ ## License
151
+
152
+ [MIT](LICENSE)
prlens-0.1.0/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # lens
2
+
3
+ AI-powered GitHub PR code reviewer for teams. Fetches a pull request, reviews each changed file against your coding guidelines using Claude or GPT-4o, and posts inline comments directly on GitHub.
4
+
5
+ ## Features
6
+
7
+ - Reviews only added/modified files — skips deleted files and binary assets
8
+ - Supports **Anthropic Claude** and **OpenAI GPT-4o** as AI backends
9
+ - Bring your own guidelines via a simple YAML config file
10
+ - Posts inline review comments on the diff using the GitHub Review API
11
+ - Runs as a **CLI tool** locally or as a **GitHub Action** automatically on every PR
12
+ - Prevents duplicate comments across repeated runs
13
+ - Filters known AI false positives (hallucinated suggestions)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install prlens
19
+ ```
20
+
21
+ ## Quick Start (CLI)
22
+
23
+ ```bash
24
+ export GITHUB_TOKEN=ghp_...
25
+ export ANTHROPIC_API_KEY=sk-ant-...
26
+
27
+ prlens --repo owner/repo --pr 42 --model anthropic
28
+ ```
29
+
30
+ Omit `--pr` to list open PRs and pick one interactively.
31
+
32
+ ## GitHub Action
33
+
34
+ Add this workflow to your repository to automatically review every pull request:
35
+
36
+ ```yaml
37
+ # .github/workflows/code-review.yml
38
+ name: Code Review
39
+
40
+ on:
41
+ pull_request:
42
+ types: [opened, synchronize]
43
+
44
+ jobs:
45
+ review:
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: jodohq/prlens/.github/actions/review@main
49
+ with:
50
+ model: anthropic
51
+ github-token: ${{ secrets.GITHUB_TOKEN }}
52
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ Copy `.prlens.example.yml` to `.prlens.yml` in your repository root:
58
+
59
+ ```yaml
60
+ # .prlens.yml
61
+ model: anthropic # anthropic | openai
62
+ max_chars_per_file: 20000
63
+ batch_limit: 60
64
+
65
+ # Path to your team's coding guidelines (relative to repo root)
66
+ # guidelines: ./docs/guidelines.md
67
+
68
+ # Files/directories to skip — fnmatch globs or directory names
69
+ # exclude:
70
+ # - migrations/
71
+ # - "*.min.js"
72
+
73
+ # Set to true to review draft PRs (skipped by default)
74
+ review_draft_prs: false
75
+ ```
76
+
77
+ The `--model` and `--guidelines` CLI flags override the config file. The config file overrides built-in defaults.
78
+
79
+ ## Custom Guidelines
80
+
81
+ Point `guidelines` in `.prlens.yml` to any Markdown file containing your team's coding standards:
82
+
83
+ ```yaml
84
+ guidelines: ./docs/guidelines.md
85
+ ```
86
+
87
+ When not set, built-in generic guidelines are used as a starting point. Copy them from [`prlens/guidelines/`](prlens/guidelines/) and customize.
88
+
89
+ ## Environment Variables
90
+
91
+ | Variable | Required | Description |
92
+ |---|---|---|
93
+ | `GITHUB_TOKEN` | Yes | GitHub personal access token with `pull_requests: write` |
94
+ | `ANTHROPIC_API_KEY` | When using Claude | Anthropic API key |
95
+ | `OPENAI_API_KEY` | When using GPT-4o | OpenAI API key |
96
+
97
+ ## CLI Reference
98
+
99
+ ```
100
+ Usage: prlens [OPTIONS]
101
+
102
+ AI-powered GitHub PR code reviewer.
103
+
104
+ Options:
105
+ --repo TEXT GitHub repository in owner/name format. [required]
106
+ --pr INTEGER Pull request number. Omit to list open PRs interactively.
107
+ --model [anthropic|openai] AI model provider. Overrides config file.
108
+ --config TEXT Path to the configuration file. [default: .prlens.yml]
109
+ -y, --yes Skip confirmation prompts and post comments automatically.
110
+ --guidelines PATH Path to a Markdown guidelines file. Overrides config file.
111
+ -s, --shadow Dry-run mode: print review comments to the terminal without posting to GitHub.
112
+ --help Show this message and exit.
113
+ ```
114
+
115
+ ## Contributing
116
+
117
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up a development environment, run tests, and add new AI providers.
118
+
119
+ ## License
120
+
121
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """lens: AI-powered GitHub PR code reviewer for teams."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,88 @@
1
+ """CLI entry point for lens."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from prlens.config import load_config
7
+ from prlens.gh.pull_request import get_repo, get_pull_requests
8
+ from prlens.reviewer import run_review
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command()
14
+ @click.option("--repo", required=True, help="GitHub repository in owner/name format.")
15
+ @click.option(
16
+ "--pr",
17
+ "pr_number",
18
+ type=int,
19
+ default=None,
20
+ help="Pull request number. Omit to list open PRs interactively.",
21
+ )
22
+ @click.option(
23
+ "--model",
24
+ type=click.Choice(["anthropic", "openai"]),
25
+ default=None,
26
+ help="AI model provider. Overrides config file.",
27
+ )
28
+ @click.option(
29
+ "--guidelines",
30
+ "guidelines_path",
31
+ default=None,
32
+ help="Path to a Markdown guidelines file. Overrides config file.",
33
+ )
34
+ @click.option(
35
+ "--config",
36
+ "config_path",
37
+ default=".prlens.yml",
38
+ show_default=True,
39
+ help="Path to the configuration file.",
40
+ )
41
+ @click.option(
42
+ "--yes",
43
+ "-y",
44
+ is_flag=True,
45
+ help="Skip confirmation prompts and post comments automatically.",
46
+ )
47
+ @click.option(
48
+ "--shadow",
49
+ "-s",
50
+ is_flag=True,
51
+ help="Dry-run mode: print review comments to the terminal without posting to GitHub.",
52
+ )
53
+ def main(repo: str, pr_number: int | None, model: str | None, guidelines_path: str | None, config_path: str, yes: bool, shadow: bool):
54
+ """AI-powered GitHub PR code reviewer.
55
+
56
+ Fetches a pull request, reviews each changed file against your coding
57
+ guidelines using Claude or GPT-4o, and posts inline comments on GitHub.
58
+
59
+ \b
60
+ Required environment variables:
61
+ GITHUB_TOKEN GitHub personal access token
62
+ ANTHROPIC_API_KEY Required when using --model anthropic
63
+ OPENAI_API_KEY Required when using --model openai
64
+ """
65
+ config = load_config(config_path, cli_overrides={"model": model, "guidelines": guidelines_path})
66
+
67
+ if not config.get("github_token"):
68
+ raise click.UsageError(
69
+ "GITHUB_TOKEN environment variable is not set. "
70
+ "Create a token at https://github.com/settings/tokens"
71
+ )
72
+ if config["model"] == "anthropic" and not config.get("anthropic_api_key"):
73
+ raise click.UsageError("ANTHROPIC_API_KEY environment variable is not set.")
74
+ if config["model"] == "openai" and not config.get("openai_api_key"):
75
+ raise click.UsageError("OPENAI_API_KEY environment variable is not set.")
76
+
77
+ if pr_number is None:
78
+ this_repo = get_repo(repo, token=config["github_token"])
79
+ prs = list(get_pull_requests(this_repo))
80
+ if not prs:
81
+ console.print("[yellow]No open pull requests found.[/yellow]")
82
+ return
83
+ console.print("\nOpen pull requests:")
84
+ for pr in prs:
85
+ console.print(f" [bold]#{pr.number}[/bold] {pr.title}")
86
+ pr_number = click.prompt("\nEnter the pull request number", type=int)
87
+
88
+ run_review(repo=repo, pr_number=pr_number, config=config, auto_confirm=yes, shadow=shadow)
@@ -0,0 +1,65 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import yaml
6
+
7
+ DEFAULT_CONFIG: dict = {
8
+ "model": "anthropic",
9
+ "max_chars_per_file": 20000,
10
+ "batch_limit": 60,
11
+ "guidelines": None, # None = use built-in default; set to a path string to override
12
+ "exclude": [], # fnmatch patterns or directory names to skip (e.g. "migrations/", "*.min.js")
13
+ "review_draft_prs": False,
14
+ }
15
+
16
+ BUILTIN_GUIDELINES_DIR = Path(__file__).parent / "guidelines"
17
+ _BUILTIN_DEFAULT = BUILTIN_GUIDELINES_DIR / "backend.md"
18
+
19
+
20
+ def load_config(config_path: str = ".prlens.yml", cli_overrides: Optional[dict] = None) -> dict:
21
+ """
22
+ Load configuration by merging (in order of precedence):
23
+ 1. Built-in defaults
24
+ 2. .prlens.yml in the current directory
25
+ 3. CLI argument overrides
26
+ """
27
+ config = {**DEFAULT_CONFIG, "exclude": list(DEFAULT_CONFIG["exclude"])}
28
+
29
+ path = Path(config_path)
30
+ if path.exists():
31
+ with open(path) as f:
32
+ file_config = yaml.safe_load(f) or {}
33
+ config.update(file_config)
34
+
35
+ if cli_overrides:
36
+ for key, value in cli_overrides.items():
37
+ if value is not None:
38
+ config[key] = value
39
+
40
+ # Resolve credentials from environment variables
41
+ config["github_token"] = os.environ.get("GITHUB_TOKEN")
42
+ config["anthropic_api_key"] = os.environ.get("ANTHROPIC_API_KEY")
43
+ config["openai_api_key"] = os.environ.get("OPENAI_API_KEY")
44
+
45
+ return config
46
+
47
+
48
+ def load_guidelines(config: dict) -> str:
49
+ """
50
+ Load review guidelines.
51
+
52
+ If ``guidelines`` is set in config, loads from that path (relative to cwd).
53
+ Otherwise falls back to the built-in default.
54
+ """
55
+ custom_path = config.get("guidelines")
56
+ if custom_path:
57
+ p = Path(custom_path)
58
+ if not p.exists():
59
+ raise FileNotFoundError(f"Guidelines file not found: {custom_path}")
60
+ return p.read_text()
61
+
62
+ if _BUILTIN_DEFAULT.exists():
63
+ return _BUILTIN_DEFAULT.read_text()
64
+
65
+ raise FileNotFoundError("No guidelines configured and built-in default is missing.")
File without changes
@@ -0,0 +1,17 @@
1
+ from github import Github
2
+
3
+
4
+ def get_repo(repo_name: str, token: str):
5
+ return Github(token).get_repo(repo_name)
6
+
7
+
8
+ def get_pull(repo, pr_number: int):
9
+ return repo.get_pull(pr_number)
10
+
11
+
12
+ def get_pull_requests(repo, state: str = "open"):
13
+ return repo.get_pulls(state=state)
14
+
15
+
16
+ def get_diff(pr):
17
+ return pr.get_files()
@@ -0,0 +1,37 @@
1
+ # Backend Code Review Guidelines
2
+
3
+ ## REST & Architecture
4
+
5
+ - Use proper HTTP verbs (e.g., `POST /users` not `POST /users/create`).
6
+ - Keep business logic in service layers — views/controllers should only orchestrate.
7
+ - Avoid putting business logic in model `save()` methods or view handlers.
8
+ - Avoid boolean flags that alter method behavior; create explicit, separate methods instead.
9
+
10
+ ## Code Structure & Reusability
11
+
12
+ - Place shared logic in a shared library if it is reused across multiple services.
13
+ - Wrap external integrations (e.g., Slack, email, storage) in clean service layers that do not depend on internal app logic.
14
+ - Avoid adding executable code in `__init__.py` files — use them only for imports and package exposure.
15
+
16
+ ## Django-Specific Practices (if applicable)
17
+
18
+ - Use `select_related` / `prefetch_related` to avoid N+1 queries.
19
+ - Validate inputs in serializers, not in views or services.
20
+ - Use custom domain exceptions instead of generic ones for consistent error handling.
21
+ - Organize code modularly by domain (e.g., `users`, `payments`, `notifications`).
22
+
23
+ ## Python Best Practices
24
+
25
+ - Use type hints in all function and method signatures.
26
+ - Avoid wildcard imports (`from module import *`).
27
+ - Use `logging` with appropriate levels instead of `print()`.
28
+ - Replace magic strings and numbers with named constants or enums.
29
+ - Prefer `pathlib.Path` over string-based file paths.
30
+ - Use context managers (`with` statements) for files and resources.
31
+
32
+ ## Testing & Maintainability
33
+
34
+ - Write unit tests for all new service logic.
35
+ - Keep tests fast and deterministic; mock all external dependencies.
36
+ - Use environment variables for secrets and configuration — never hardcode them.
37
+ - Add docstrings and API documentation for new public endpoints and logic.
@@ -0,0 +1,65 @@
1
+ # Frontend Code Review Guidelines
2
+
3
+ ## API Handling
4
+
5
+ - Store API responses in global state (e.g., Redux) only if needed across multiple components.
6
+ - Use component-local state (`useState`/`useEffect`) for view-specific or session-specific data.
7
+ - Avoid flag-based conditional API logic inside components — extract it to helper functions or hooks.
8
+ - Optimize for performance: debounce search inputs, paginate large datasets, cache static responses.
9
+
10
+ ## State Management
11
+
12
+ - Use global state slices for shared state only.
13
+ - Avoid duplicating state between global state and component-local state.
14
+ - Encapsulate side effects and data-fetching in reusable custom hooks.
15
+
16
+ ## Component Architecture
17
+
18
+ - Follow a clean separation of concerns:
19
+ - `components/` — Dumb, reusable UI elements
20
+ - `containers/` — Smart components with data-fetching
21
+ - `hooks/` — Reusable logic for side effects
22
+ - `utils/`, `constants/` — Low-level modules and config
23
+
24
+ ## Do's
25
+
26
+ - Use constants or enums for repeated value-label pairs (e.g., statuses, categories).
27
+ - Keep components small and composable.
28
+ - Write tests for custom hooks, logic, and critical UI behaviors.
29
+
30
+ ## Don'ts
31
+
32
+ - Don't use wildcard imports (e.g., `import * as lib`) — prefer named imports.
33
+ - Don't hardcode magic values — define them as constants or enums.
34
+ - Don't embed conditional API logic directly in components.
35
+ - Don't bloat container components — move logic to hooks or services.
36
+
37
+ ## Value-to-Label Mapping
38
+
39
+ Use a structured class with static getters for value-label constants:
40
+
41
+ ```js
42
+ // Good
43
+ class DocumentType {
44
+ static get PASSPORT() {
45
+ return { code: "passport", title: "Passport" };
46
+ }
47
+ static get ALL() {
48
+ return [DocumentType.PASSPORT];
49
+ }
50
+ }
51
+
52
+ // Bad
53
+ const DocumentType = {
54
+ PASSPORT: { code: "passport", title: "Passport" },
55
+ };
56
+ ```
57
+
58
+ Using a class prevents unintentional mutation and supports lazy initialization.
59
+
60
+ ## Syntax & Language Notes
61
+
62
+ - Use named imports: `import { Button } from "antd";`
63
+ - Avoid wildcard imports: `import * as antd from "antd";`
64
+ - Do not suggest removing fallback logic (e.g., `|| []`) unless the value is guaranteed non-null.
65
+ Note: JavaScript's `Map.get()` does not support default values like Python's `dict.get()`.
File without changes
@@ -0,0 +1,114 @@
1
+ import json
2
+ import logging
3
+ import time
4
+
5
+ from anthropic import Anthropic
6
+ from anthropic.types import TextBlock
7
+
8
+ from prlens.providers.base import BaseReviewer
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AnthropicReviewer(BaseReviewer):
14
+ MODEL = "claude-sonnet-4-20250514"
15
+ MAX_TOKENS = 4096
16
+ MAX_RETRIES = 3
17
+
18
+ def __init__(self, api_key: str):
19
+ self.client = Anthropic(api_key=api_key)
20
+
21
+ def review(
22
+ self,
23
+ description: str,
24
+ file_name: str,
25
+ diff_patch: str,
26
+ file_content: str,
27
+ guidelines: str,
28
+ ) -> list[dict]:
29
+ prompt = self._build_prompt(description, file_name, diff_patch, file_content, guidelines)
30
+ raw = self._call_with_retry(prompt)
31
+ if raw is None:
32
+ return []
33
+ return self._parse(raw)
34
+
35
+ def _call_with_retry(self, prompt: str) -> str | None:
36
+ for attempt in range(self.MAX_RETRIES):
37
+ try:
38
+ response = self.client.messages.create(
39
+ model=self.MODEL,
40
+ messages=[{"role": "user", "content": prompt}],
41
+ temperature=0.3,
42
+ max_tokens=self.MAX_TOKENS,
43
+ )
44
+ text_blocks = [block.text for block in response.content if isinstance(block, TextBlock)]
45
+ return "".join(text_blocks).strip()
46
+ except Exception as e:
47
+ if attempt == self.MAX_RETRIES - 1:
48
+ logger.error("Anthropic API failed after %d attempts: %s", self.MAX_RETRIES, e)
49
+ return None
50
+ delay = 2**attempt
51
+ logger.warning(
52
+ "Anthropic API error (attempt %d/%d): %s. Retrying in %ds...",
53
+ attempt + 1,
54
+ self.MAX_RETRIES,
55
+ e,
56
+ delay,
57
+ )
58
+ time.sleep(delay)
59
+
60
+ def _build_prompt(
61
+ self, description: str, file_name: str, diff_patch: str, file_content: str, guidelines: str
62
+ ) -> str:
63
+ return f"""
64
+ You are a strict and precise senior code reviewer.
65
+ Review the patch below and identify issues according to the guidelines.
66
+
67
+ {guidelines}
68
+
69
+ Rules:
70
+ - Focus on added lines (starting with '+') for direct violations.
71
+ - Also consider implications of removed lines (starting with '-') — e.g. deleted null checks, removed error handling, dropped permission guards.
72
+ - Do not comment on code that already follows best practices.
73
+ - Avoid assumptions when context is unclear. Be concise and actionable.
74
+
75
+ You are reviewing file `{file_name}`.
76
+
77
+ PR description:
78
+ {description}
79
+
80
+ Diff:
81
+ {diff_patch}
82
+
83
+ File content (for context):
84
+ {file_content}
85
+
86
+ ### Output Format:
87
+ Respond with **only** a valid JSON list:
88
+
89
+ [
90
+ {{
91
+ "line": <line number in the new file (integer)>,
92
+ "severity": "<critical|major|minor|nitpick>",
93
+ "comment": "<concise, actionable comment>"
94
+ }},
95
+ ...
96
+ ]
97
+
98
+ Severity guide:
99
+ - critical: security vulnerability, data loss risk, crash
100
+ - major: logic bug, missing error handling, significant performance issue
101
+ - minor: code smell, unclear naming, missing type hint
102
+ - nitpick: style preference, minor formatting
103
+
104
+ If there are no issues, return: []
105
+ Do not return any text outside the JSON block.
106
+ """
107
+
108
+ def _parse(self, raw: str) -> list[dict]:
109
+ try:
110
+ cleaned = raw.replace("```json", "").replace("```", "").strip()
111
+ return json.loads(cleaned)
112
+ except json.JSONDecodeError:
113
+ logger.warning("Failed to parse Anthropic response as JSON: %s", raw[:200])
114
+ return []