Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab6f62d2f | ||
|
|
b969ac5744 |
No files matched your search
@@ -1,629 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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@v6.0.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6.2.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@v7.0.0
|
|
||||||
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
|
|
||||||
@@ -264,5 +264,6 @@ tmp_test
|
|||||||
vllm_omni/_version.py
|
vllm_omni/_version.py
|
||||||
# output files
|
# output files
|
||||||
*.wav
|
*.wav
|
||||||
|
.worktrees/
|
||||||
# CI overlay yamls materialized from tests/utils.py:_CI_OVERLAYS at test time
|
# CI overlay yamls materialized from tests/utils.py:_CI_OVERLAYS at test time
|
||||||
tests/.ci_generated/
|
tests/.ci_generated/
|
||||||
@@ -81,7 +81,6 @@ nav:
|
|||||||
- Developer Guide:
|
- Developer Guide:
|
||||||
- General:
|
- General:
|
||||||
- contributing/README.md
|
- contributing/README.md
|
||||||
- pr_reviewer.md
|
|
||||||
- glob: contributing/*
|
- glob: contributing/*
|
||||||
flatten_single_child_sections: true
|
flatten_single_child_sections: true
|
||||||
- Model Implementation:
|
- Model Implementation:
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -283,3 +283,10 @@ Using Cache-DiT acceleration:
|
|||||||
|
|
||||||
1. ✅ **Enable Cache-DiT** - Set `cache_backend="cache_dit"` to get 1.5x-3x speedup with optimized defaults
|
1. ✅ **Enable Cache-DiT** - Set `cache_backend="cache_dit"` to get 1.5x-3x speedup with optimized defaults
|
||||||
2. ✅ **(Optional) Customize** - Adjust `cache_config` parameters for specific speed/quality trade-offs
|
2. ✅ **(Optional) Customize** - Adjust `cache_config` parameters for specific speed/quality trade-offs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Cache-DiT documentation](https://cache-dit.readthedocs.io/en/latest/)
|
||||||
|
- [Cache-DiT API reference](https://cache-dit.readthedocs.io/en/latest/user_guide/CACHE_API/)
|
||||||
@@ -97,6 +97,7 @@ plugins:
|
|||||||
exclude:
|
exclude:
|
||||||
- "re:vllm_omni\\._.*" # Internal modules
|
- "re:vllm_omni\\._.*" # Internal modules
|
||||||
- "vllm_omni.diffusion.models.qwen_image" # avoid importing vllm in mkdocs building
|
- "vllm_omni.diffusion.models.qwen_image" # avoid importing vllm in mkdocs building
|
||||||
|
- "vllm_omni.diffusion.models.dreamid_omni.wan2_2" # docstring signature warnings break strict docs
|
||||||
- "vllm_omni.diffusion.quantization" # avoid importing vllm in mkdocs building
|
- "vllm_omni.diffusion.quantization" # avoid importing vllm in mkdocs building
|
||||||
- "vllm_omni.quantization" # avoid importing vllm in mkdocs building
|
- "vllm_omni.quantization" # avoid importing vllm in mkdocs building
|
||||||
- "vllm_omni.entrypoints.async_diffusion" # avoid importing vllm in mkdocs building
|
- "vllm_omni.entrypoints.async_diffusion" # avoid importing vllm in mkdocs building
|
||||||
|
|||||||
Reference in new issue
Block a user