diff --git a/.github/scripts/pr_reviewer.py b/.github/scripts/pr_reviewer.py new file mode 100755 index 000000000..da629a645 --- /dev/null +++ b/.github/scripts/pr_reviewer.py @@ -0,0 +1,629 @@ +#!/usr/bin/env python3 +""" +PR Reviewer using GLM API for vllm-omni project. +""" + +import json +import logging +import os +import sys +import time +from dataclasses import dataclass +from typing import Any, TypedDict + +import requests + + +# Type definitions for API responses +class PRDetails(TypedDict): + """Type definition for GitHub PR details response.""" + + title: str + body: str + number: int + state: str + user: dict[str, Any] + + +class GLMMessage(TypedDict): + """Type definition for GLM API message.""" + + role: str + content: str + + +class GLMChoice(TypedDict): + """Type definition for GLM API choice.""" + + message: GLMMessage + finish_reason: str + + +class GLMResponse(TypedDict): + """Type definition for GLM API response.""" + + choices: list[GLMChoice] + usage: dict[str, int] | None + + +class GitHubComment(TypedDict): + """Type definition for GitHub comment.""" + + id: int + body: str + created_at: str + user: dict[str, Any] + + +# Configuration +TRIGGER_PHRASE: str = "@vllm-omni-reviewer" +DEFAULT_GLM_API_URL: str = "https://open.bigmodel.cn/api/paas/v4/chat/completions" # noqa: E501 +DEFAULT_GLM_MODEL: str = "glm-5" +DEFAULT_COOLDOWN_MINUTES: int = 5 +DEFAULT_MAX_RETRIES: int = 3 +DEFAULT_RETRY_DELAY: float = 1.0 +MAX_DIFF_SIZE: int = 100_000 # Maximum diff size in characters + + +@dataclass +class Config: + """Configuration for the PR reviewer.""" + + glm_api_url: str + glm_model: str + cooldown_minutes: int + max_retries: int + retry_delay: float + max_diff_size: int + + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format="[PR Reviewer] %(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger: logging.Logger = logging.getLogger(__name__) + + +def get_config() -> Config: + """Load configuration from environment variables with defaults.""" + return Config( + glm_api_url=os.getenv("GLM_API_URL", DEFAULT_GLM_API_URL), + glm_model=os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL), + cooldown_minutes=int( + os.getenv( + "PR_REVIEWER_COOLDOWN_MINUTES", + str(DEFAULT_COOLDOWN_MINUTES), + ) + ), + max_retries=int( + os.getenv( + "PR_REVIEWER_MAX_RETRIES", + str(DEFAULT_MAX_RETRIES), + ) + ), + retry_delay=float(os.getenv("PR_REVIEWER_RETRY_DELAY", str(DEFAULT_RETRY_DELAY))), + max_diff_size=int(os.getenv("PR_REVIEWER_MAX_DIFF_SIZE", str(MAX_DIFF_SIZE))), # noqa: E501 + ) + + +def get_env_var(name: str) -> str: + """ + Get an environment variable or raise an error. + + Args: + name: Name of the environment variable. + + Returns: + The value of the environment variable. + + Raises: + SystemExit: If the environment variable is not set. + """ + value = os.environ.get(name) + if not value: + logger.error(f"Environment variable {name} is not set") + sys.exit(1) + return value + + +def check_trigger(comment_body: str) -> bool: + """ + Check if the comment contains the trigger phrase. + + Args: + comment_body: The body of the comment to check. + + Returns: + True if the trigger phrase is found, False otherwise. + """ + return TRIGGER_PHRASE in comment_body + + +def fetch_pr_diff( + repo_name: str, + pr_number: int, + token: str, + max_size: int = MAX_DIFF_SIZE, +) -> str | None: + """ + Fetch the diff for a pull request. + + Args: + repo_name: The repository name in format "owner/repo". + pr_number: The pull request number. + token: GitHub authentication token. + max_size: Maximum diff size in characters. + + Returns: + The diff content as a string, or None if fetching failed. + Returns empty string if diff is larger than max_size. + """ + url: str = f"https://api.github.com/repos/{repo_name}/pulls/{pr_number}" + headers: dict[str, str] = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3.diff", + } + + logger.info(f"Fetching PR diff from {url}") + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + diff: str = response.text + if len(diff) > max_size: + logger.warning( + f"Diff size ({len(diff)} bytes) exceeds maximum " + f"({max_size} bytes), truncating to first " + f"{max_size} bytes" + ) + return diff[:max_size] + "\n\n... [Diff truncated due to size] ..." + logger.info(f"Successfully fetched diff ({len(diff)} bytes)") + return diff + else: + logger.error(f"Failed to fetch PR diff: {response.status_code}") + logger.error(f"Response: {response.text}") + return None + + +def fetch_pr_details( + repo_name: str, + pr_number: int, + token: str, +) -> PRDetails | None: + """ + Fetch PR details including title and description. + + Args: + repo_name: The repository name in format "owner/repo". + pr_number: The pull request number. + token: GitHub authentication token. + + Returns: + A dictionary containing PR details, or None if fetching failed. + """ + url: str = f"https://api.github.com/repos/{repo_name}/pulls/{pr_number}" + headers: dict[str, str] = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + } + + logger.info(f"Fetching PR details from {url}") + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + return response.json() + else: + logger.error(f"Failed to fetch PR details: {response.status_code}") + return None + + +def build_review_prompt(pr_title: str, pr_description: str, diff: str) -> str: + """ + Build the prompt for the GLM-4.7 API. + + Args: + pr_title: The title of the pull request. + pr_description: The description/body of the pull request. + diff: The diff content of the pull request. + + Returns: + The formatted prompt string for the API. + """ + return f"""You are an expert code reviewer for the VLLM-Omni project. \ +Please review the following pull request: + +## Pull Request Details +**Title:** {pr_title} + +**Description:** +{pr_description if pr_description else "No description provided."} + +## Code Changes (Diff) +{diff} + +## Review Guidelines + +Please provide a comprehensive code review with the following sections: + +### 1. Overview +- Brief summary of the changes +- Overall assessment (positive, neutral, or concerns) + +### 2. Code Quality +- Code style and consistency +- Potential bugs or edge cases +- Performance considerations +- Error handling + +### 3. Architecture & Design +- Integration with existing codebase +- Design patterns and best practices +- Potential improvements + +### 4. Security & Safety +- Security concerns (if any) +- Resource management +- Input validation + +### 5. Testing & Documentation +- Test coverage considerations +- Documentation completeness +- Examples and usage clarity + +### 6. Specific Suggestions +- Line-by-line specific feedback (use `file:line` format) +- Concrete actionable suggestions +- Code examples for improvements (if applicable) + +### 7. Approval Status +- **LGTM** (Looks Good To Me) if the PR is ready to merge +- **LGTM with suggestions** if the PR is good but has minor suggestions +- **Changes requested** if significant changes are needed + +## Important Notes +- Be constructive and helpful +- Focus on objective technical feedback +- Acknowledge good practices when you see them +- Prioritize critical issues over nitpicks +- If the diff is empty or minimal, acknowledge this and provide + any relevant context-specific guidance + +Please format your response in Markdown with clear section headers. +""" + + +def validate_glm_response(data: dict[str, Any]) -> str | None: + """ + Validate and extract content from GLM API response. + + Args: + data: The response data from GLM API. + + Returns: + The review content string if valid, None otherwise. + """ + # Check if choices exists and is a non-empty list + if "choices" not in data: + logger.error("GLM API response missing 'choices' field") + logger.error(f"Response structure: {json.dumps(data, indent=2)}") + return None + + choices = data["choices"] + if not isinstance(choices, list): + logger.error(f"GLM API 'choices' is not a list: {type(choices)}") + return None + + if len(choices) == 0: + logger.error("GLM API 'choices' is an empty list") + return None + + # Check if first choice has message + try: + first_choice = choices[0] + if not isinstance(first_choice, dict): + logger.error(f"GLM API choice is not a dict: {type(first_choice)}") + return None + + if "message" not in first_choice: + logger.error("GLM API choice missing 'message' field") + logger.error(f"Choice structure: {json.dumps(first_choice, indent=2)}") # noqa: E501 + return None + + message = first_choice["message"] + if not isinstance(message, dict): + logger.error(f"GLM API message is not a dict: {type(message)}") + return None + + if "content" not in message: + logger.error("GLM API message missing 'content' field") + logger.error(f"Message structure: {json.dumps(message, indent=2)}") + return None + + content = message["content"] + if not isinstance(content, str): + logger.error(f"GLM API content is not a string: {type(content)}") + return None + + return content + + except (KeyError, IndexError, TypeError) as e: + logger.error(f"Failed to parse GLM API response: {e}") + logger.error(f"Response: {json.dumps(data, indent=2)}") + return None + + +def call_glm_api(prompt: str, api_key: str, config: Config) -> str | None: + """ + Call the GLM-4.7 API to get code review with retry logic. + + Args: + prompt: The prompt to send to the API. + api_key: The GLM API key. + config: Configuration object. + + Returns: + The review content as a string, or None if all retries failed. + """ + headers: dict[str, str] = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + payload: dict[str, Any] = { + "model": config.glm_model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.3, + "max_tokens": 32000, + "top_p": 0.9, + } + + last_error: str | None = None + + for attempt in range(config.max_retries): + try: + logger.info(f"Calling GLM API ({config.glm_model}) - Attempt {attempt + 1}/{config.max_retries}") + response = requests.post( + config.glm_api_url, + headers=headers, + json=payload, + timeout=120, + ) + + if response.status_code == 200: + data = response.json() + review = validate_glm_response(data) + if review: + logger.info(f"Successfully received review ({len(review)} chars)") # noqa: E501 + return review + else: + last_error = "Failed to validate API response structure" + logger.error(last_error) + else: + last_error = f"GLM API request failed: {response.status_code} - {response.text}" + logger.error(last_error) + + except requests.exceptions.Timeout: + last_error = f"GLM API request timed out (attempt {attempt + 1})" + logger.error(last_error) + except requests.exceptions.RequestException as e: + last_error = f"GLM API request exception: {e}" + logger.error(last_error) + except json.JSONDecodeError as e: + last_error = f"Failed to decode GLM API response as JSON: {e}" + logger.error(last_error) + + # Exponential backoff before retry + if attempt < config.max_retries - 1: + wait_time: float = config.retry_delay * (2**attempt) + logger.info(f"Waiting {wait_time}s before retry...") # noqa: E501 + time.sleep(wait_time) + + logger.error( + f"All {config.max_retries} attempts failed. Last error: {last_error}" # noqa: E501 + ) + return None + + +def check_cooldown( # noqa: E501 + repo_name: str, + pr_number: int, + token: str, + cooldown_minutes: int, +) -> bool: + """ + Check if the PR is within the cooldown period. + + Args: + repo_name: The repository name in format "owner/repo". + pr_number: The pull request number. + token: GitHub authentication token. + cooldown_minutes: Cooldown period in minutes. + + Returns: + True if within cooldown period (should skip), False otherwise. + """ + from datetime import datetime, timedelta + + url: str = ( + f"https://api.github.com/repos/{repo_name}/issues/" + f"{pr_number}/comments" # noqa: E501 + ) + headers: dict[str, str] = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + } + + logger.info(f"Checking cooldown period ({cooldown_minutes} minutes)") + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code != 200: + logger.warning(f"Failed to check cooldown: {response.status_code}, proceeding with review") + return False + + comments: list[dict[str, Any]] = response.json() + cutoff_time: datetime = datetime.utcnow() - timedelta(minutes=cooldown_minutes) # noqa: E501 + + for comment in reversed(comments): + # Check if this is a bot comment + body: str = comment.get("body", "") + if "VLLM-Omni PR Review" in body or "PR Reviewer Bot" in body: + created_at_str: str = comment.get("created_at", "") + try: + # Parse GitHub timestamp format + created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00")) + created_at = created_at.replace(tzinfo=None) + if created_at > cutoff_time: + logger.info(f"PR is within cooldown period (last review: {created_at_str})") + return True + except ValueError: + logger.warning(f"Failed to parse comment timestamp: {created_at_str}") # noqa: E501 + continue + + logger.info("PR is outside cooldown period, proceeding with review") + return False + + +def post_review_comment( # noqa: E501 + repo_name: str, + pr_number: int, + token: str, + review: str, +) -> bool: + """ + Post the review as a comment on the PR. + + Args: + repo_name: The repository name in format "owner/repo". + pr_number: The pull request number. + token: GitHub authentication token. + review: The review content to post. + + Returns: + True if posting succeeded, False otherwise. + """ + url: str = ( + f"https://api.github.com/repos/{repo_name}/issues/" + f"{pr_number}/comments" # noqa: E501 + ) + headers: dict[str, str] = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + } + + # Format the review comment + comment_body: str = f"""## 🤖 VLLM-Omni PR Review + +{review} + +--- +*This review was generated automatically by the VLLM-Omni PR Reviewer Bot +using {os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL)}.* +""" + + payload: dict[str, str] = {"body": comment_body} + + logger.info(f"Posting review comment to PR #{pr_number}") + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 201: + logger.info("Successfully posted review comment") + return True + else: + logger.error(f"Failed to post comment: {response.status_code}") + logger.error(f"Response: {response.text}") + return False + + +def main() -> int: + """ + Main entry point for the PR reviewer bot. + + Returns: + 0 on success, 1 on error. + """ + logger.info("VLLM-Omni PR Reviewer Bot starting...") + + # Load configuration + config: Config = get_config() + logger.info( + f"Configuration: model={config.glm_model}, " + f"cooldown={config.cooldown_minutes}min, " + f"max_retries={config.max_retries}" + ) + + # Get environment variables + token: str = get_env_var("GITHUB_TOKEN") + api_key: str = get_env_var("GLM_API_KEY") + repo_name: str = get_env_var("REPO_NAME") + pr_number_str: str = get_env_var("PR_NUMBER") + comment_body: str = get_env_var("COMMENT_BODY") + + try: + pr_number: int = int(pr_number_str) + except ValueError: + logger.error(f"Invalid PR number: {pr_number_str}") + return 1 + + logger.info(f"Repository: {repo_name}") + logger.info(f"PR Number: {pr_number}") + + # Check if the comment contains the trigger phrase + if not check_trigger(comment_body): + logger.info( + f"Comment does not contain trigger phrase '{TRIGGER_PHRASE}', exiting" # noqa: E501 + ) + return 0 + + logger.info("Trigger phrase detected! Starting review process...") + + # Check cooldown period + if check_cooldown(repo_name, pr_number, token, config.cooldown_minutes): + logger.info("Skipping review due to cooldown period") + return 0 + + # Fetch PR details + logger.info("Step 1/4: Fetching PR details...") + pr_details: PRDetails | None = fetch_pr_details(repo_name, pr_number, token) # noqa: E501 + if not pr_details: + logger.error("Failed to fetch PR details") + return 1 + + pr_title: str = pr_details.get("title", "Unknown") + pr_description: str = pr_details.get("body", "") + + logger.info(f"PR Title: {pr_title}") + + # Fetch PR diff + logger.info("Step 2/4: Fetching PR diff...") + diff: str | None = fetch_pr_diff(repo_name, pr_number, token, config.max_diff_size) + if diff is None: + logger.error("Failed to fetch PR diff") + return 1 + + if not diff: + logger.warning("Warning: Empty diff - this might be a draft PR or no code changes") + + # Build prompt + logger.info("Step 3/4: Building review prompt...") + prompt: str = build_review_prompt(pr_title, pr_description, diff) + + # Call GLM API + logger.info("Step 4/4: Calling GLM API...") + review: str | None = call_glm_api(prompt, api_key, config) + if not review: + logger.error("Failed to get review from GLM API") + return 1 + + # Post review comment + logger.info("Posting review comment...") + if not post_review_comment(repo_name, pr_number, token, review): + logger.error("Failed to post review comment") + return 1 + + logger.info("PR review completed successfully!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/pr-reviewer.yml b/.github/workflows/pr-reviewer.yml new file mode 100644 index 000000000..a503d3422 --- /dev/null +++ b/.github/workflows/pr-reviewer.yml @@ -0,0 +1,62 @@ +name: VLLM-Omni PR Reviewer + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + pr-reviewer: + name: Review Pull Request + runs-on: ubuntu-latest + timeout-minutes: 10 + # Only run when the comment is from a collaborator/owner/member + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && + contains(github.event.comment.body, '@vllm-omni-reviewer') && + (github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' || + github.event.comment.author_association == 'OWNER') + steps: + - name: Checkout repository + uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5.3.0 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip==24.0 + pip install requests==2.31.0 pyyaml==6.0.1 + + - name: Run PR Reviewer + id: reviewer + env: + GLM_API_KEY: ${{ secrets.GLM_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + COMMENT_BODY: ${{ github.event.comment.body }} + REPO_NAME: ${{ github.repository }} + PR_REVIEWER_COOLDOWN_MINUTES: 5 + PR_REVIEWER_MAX_RETRIES: 3 + run: | + python .github/scripts/pr_reviewer.py 2>&1 | tee "/tmp/pr_review_${PR_NUMBER}.log" + + - name: Upload review logs + if: always() + uses: actions/upload-artifact@v4.4.3 + with: + name: pr-review-logs-${{ github.event.issue.number || github.event.pull_request.number }} + path: /tmp/pr_review_*.log + retention-days: 7 + if-no-files-found: ignore diff --git a/docs/pr_reviewer.md b/docs/pr_reviewer.md new file mode 100644 index 000000000..ad3235532 --- /dev/null +++ b/docs/pr_reviewer.md @@ -0,0 +1,255 @@ +# VLLM-Omni PR Reviewer + +## Overview + +The VLLM-Omni PR Reviewer is an automated code review bot powered by GLM-4.7 AI model. It helps maintain code quality by providing intelligent feedback on pull requests. + +## Features + +- **Intelligent Code Analysis**: Leverages GLM-4.7 for understanding code context and providing meaningful feedback +- **Comprehensive Reviews**: Covers code quality, architecture, security, testing, and documentation +- **Structured Output**: Provides well-formatted reviews with clear sections and actionable suggestions +- **Rate Limiting**: Built-in cooldown mechanism to prevent excessive API usage +- **Retry Logic**: Automatic retries with exponential backoff for transient API failures +- **Defensive Parsing**: Robust validation of API responses to handle malformed data +- **Cost Control**: Only repository members/collaborators/owners can trigger reviews + +## How to Use + +### Triggering a Review + +To trigger an automated PR review, mention the bot in a PR comment: + +``` +@vllm-omni-reviewer please review +``` + +Or include in your PR description: + +``` +@vllm-omni-reviewer +``` + +The bot will automatically review your changes and post a detailed comment. + +## What Gets Reviewed + +- **vLLM Architecture Compatibility**: Ensures changes align with vLLM's design patterns +- **Multi-modal Integration**: Reviews audio, vision, and text processing implementations +- **Performance Implications**: Analyzes impact on inference latency and throughput +- **Code Quality**: Checks Python best practices, type hints, and documentation +- **Security Considerations**: Identifies potential security vulnerabilities +- **Testing Coverage**: Recommends additional test cases when needed + +## Review Output + +The bot posts a structured review comment with: + +- **Overview**: Brief summary of the PR's purpose +- **Critical Issues (Must Fix)**: Blocking issues that need to be addressed +- **Important Issues (Should Fix)**: Significant concerns that should be resolved +- **Minor Issues & Suggestions**: Small improvements and optional suggestions +- **Positive Aspects**: Highlights well-implemented features +- **Performance Considerations**: Analysis of performance impact +- **Testing Recommendations**: Suggestions for additional tests +- **Overall Assessment**: Final recommendation (Approve/Request Changes/Needs Major Work) + +## Rate Limiting and Cooldown + +The bot includes a cooldown mechanism to prevent excessive API usage: + +- **Default cooldown**: 5 minutes between reviews per PR +- **Configurable**: Can be adjusted via `PR_REVIEWER_COOLDOWN_MINUTES` environment variable +- **Smart detection**: Checks for previous bot comments before starting a review + +If you trigger a review within the cooldown period, the bot will log a message and skip the review. + +## Architecture + +``` +┌─────────────────┐ +│ PR Comment │ +│ @vllm-omni- │ +│ reviewer │ +└────────┬────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ GitHub Actions Workflow │ +│ (.github/workflows/ │ +│ pr-reviewer.yml) │ +│ │ +│ - Python 3.11 │ +│ - requests==2.31.0 │ +│ - pyyaml==6.0.1 │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ PR Reviewer Script │ +│ (.github/scripts/ │ +│ pr_reviewer.py) │ +│ │ +│ 1. Check cooldown │ +│ 2. Fetch PR details & diff │ +│ 3. Build review prompt │ +│ 4. Call GLM-4.7 API │ +│ (with retry logic) │ +│ 5. Validate response │ +│ 6. Post review comment │ +└────────┬────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ GLM-4.7 API │ +│ (open.bigmodel.cn) │ +└─────────────────────────────────┘ +``` + +1. **GitHub Actions Workflow** (`.github/workflows/pr-reviewer.yml`): Triggers on @mention +2. **Python Script** (`.github/scripts/pr_reviewer.py`): Fetches PR data and calls GLM-4.7 API +3. **GLM-4.7 API**: Provides intelligent code analysis + +## Testing + +### Testing the PR Reviewer Bot + +To test the PR reviewer bot before deploying to production: + +1. **Create a test PR** - Make a small, safe change (e.g., documentation update) +2. **Open the PR** - Create a pull request with a descriptive title +3. **Trigger the review** - Comment `@vllm-omni-reviewer` on the PR +4. **Monitor results** - Check the Actions tab for workflow execution logs + +### Running Unit Tests + +The bot includes comprehensive unit tests that can be run locally: + +```bash +# Run all tests +pytest .github/tests/test_pr_reviewer.py -v + +# Run specific test +pytest .github/tests/test_pr_reviewer.py::TestCheckTrigger -v + +# Run with coverage +pytest .github/tests/test_pr_reviewer.py --cov=.github/scripts/pr_reviewer.py --cov-report=term-missing +``` + +### What to Look For + +When testing, verify that: +- [ ] The workflow triggers on the `@vllm-omni-reviewer` comment +- [ ] The cooldown mechanism works correctly +- [ ] The GLM API call completes without errors (with retry if needed) +- [ ] A review comment is posted to the PR +- [ ] The review content is meaningful and well-structured +- [ ] The cost is within the expected range (0.50-5 CNY) + +### Safe Test Changes + +For testing, consider making these types of safe changes: +- Documentation updates (like adding this Testing section) +- Comment improvements +- README enhancements +- Non-functional file additions + +### Example Test PR + +A good test PR might: +- Update a documentation file +- Add explanatory comments +- Improve code formatting +- Fix a minor typo + +These changes are safe to merge if the test is successful and won't affect functionality. + +## Troubleshooting + +### Bot Doesn't Respond + +1. **Check permissions** - Verify you have Owner/Member/Collaborator access +2. **Check Actions tab** - Look for workflow execution and view logs +3. **Check cooldown** - If another review was posted recently, wait for the cooldown period +4. **Check API key** - Ensure `GLM_API_KEY` is configured in repository secrets + +### API Errors + +If the GLM API call fails: +- Check the Actions tab for detailed error logs +- Verify the `GLM_API_KEY` secret is correctly configured +- Ensure sufficient API quota is available +- The bot will automatically retry up to 3 times with exponential backoff + +### Review Seems Truncated + +If the review appears incomplete: +- Large diffs may be truncated at 100,000 characters +- Check the logs for truncation warnings +- Consider breaking large PRs into smaller chunks + +## Configuration + +### Required Secrets + +The following secret must be configured in the repository settings: + +- `GLM_API_KEY` - Your GLM (BigModel) API key for accessing the GLM-4.7 API + +To add the secret: +1. Go to repository Settings → Secrets and variables → Actions +2. Click "New repository secret" +3. Name: `GLM_API_KEY` +4. Value: Your GLM API key + +### Optional Configuration + +The following optional environment variables can be set in the workflow file: + +| Variable | Default | Description | +|----------|---------|-------------| +| `GLM_API_URL` | `https://open.bigmodel.cn/api/paas/v4/chat/completions` | GLM API endpoint | +| `GLM_MODEL` | `glm-4.7` | Model to use for reviews | +| `PR_REVIEWER_COOLDOWN_MINUTES` | `5` | Cooldown period between reviews | +| `PR_REVIEWER_MAX_RETRIES` | `3` | Maximum API retry attempts | +| `PR_REVIEWER_RETRY_DELAY` | `1.0` | Base delay for retry backoff (seconds) | +| `PR_REVIEWER_MAX_DIFF_SIZE` | `100000` | Maximum diff size before truncation | + +### Workflow Customization + +The workflow can be customized in `.github/workflows/pr-reviewer.yml`: +- Change Python version (default: 3.11) +- Adjust timeout value (default: 10 minutes) +- Modify trigger conditions +- Add additional dependencies + +## Code Quality + +The PR reviewer script follows vllm-omni coding standards: + +- **Type hints**: All functions have complete type hints following mypy strict mode +- **Logging**: Uses Python's logging module for structured logging +- **Testing**: Comprehensive unit tests with pytest +- **Pre-commit**: Script is checked by pre-commit hooks (flake8) + +## Cost Estimate + +| Component | Cost | +|-----------|------| +| GitHub Actions (public repo) | Free | +| GLM API (glm-4.7) | ~0.50-5 CNY per PR (varies by size) | +| Total (20 PRs/month) | ~10-100 CNY/month (~$2-15 USD) | + +## Contributing + +To improve the PR reviewer bot: + +1. Edit `.github/scripts/pr_reviewer.py` for logic changes +2. Edit `.github/workflows/pr-reviewer.yml` for workflow changes +3. Add tests to `.github/tests/test_pr_reviewer.py` +4. Run `pre-commit run --files .github/scripts/pr_reviewer.py` to check code quality +5. Test thoroughly with a test PR before deploying to production + +## License + +This bot is part of the VLLM-Omni project and follows the same license terms.