diff --git a/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc b/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc deleted file mode 100644 index fbe98ab6..00000000 Binary files a/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc and /dev/null differ diff --git a/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc b/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc deleted file mode 100644 index 34ee8b52..00000000 Binary files a/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc and /dev/null differ diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml new file mode 100644 index 00000000..fd7ce3ef --- /dev/null +++ b/.github/workflows/ai-review.yml @@ -0,0 +1,32 @@ +name: AI Code Reviewer + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install Dependencies + run: | + pip install -r scripts/requirements.txt + + - name: Run AI Reviewer + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + run: | + python scripts/ai_reviewer.py diff --git a/.gitignore b/.gitignore index 4f160266..d235da80 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .temp_ag_kit/ antigravity-doc +__pycache__ diff --git a/docs/PLAN-ai-code-reviewer.md b/docs/PLAN-ai-code-reviewer.md new file mode 100644 index 00000000..b5898ad1 --- /dev/null +++ b/docs/PLAN-ai-code-reviewer.md @@ -0,0 +1,38 @@ +# Plan: Custom AI Code Reviewer (GitHub Actions + OpenRouter) + +## Goal Description +Implement AI Code Reviewer using **OpenRouter** (Mistral) instead of Gemini. Fix dependency conflicts and ensure compliance with OpenRouter API standards. + +## User Review Required +> [!IMPORTANT] +> **API Key**: Ensure `OPENROUTER_API_KEY` is set in GitHub Secrets. +> **Model**: Using `mistralai/devstral-2512:free` as requested (Note: verification of exact model ID recommended). + +## Proposed Changes + +### Dependencies +#### [MODIFY] [requirements.txt](file:///Users/nghialuutrung/Desktop/antigravity-kit/scripts/requirements.txt) +* Update `openai>=1.55.0` to resolve `httpx` proxy argument conflict. +* Keep `PyGithub`. + +### Logic Script +#### [MODIFY] [ai_reviewer.py](file:///Users/nghialuutrung/Desktop/antigravity-kit/scripts/ai_reviewer.py) +* **Client Initialization**: + ```python + client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=api_key, + default_headers={ + "HTTP-Referer": "https://github.com/hapo-nghialuu/antigravity-kit", # Attribution + "X-Title": "Antigravity AI Reviewer" + } + ) + ``` +* **Model**: Set `MODEL_NAME = "mistralai/devstral-2512:free"`. +* **JSON Handling**: Add robust try-catch for JSON parsing as free models might chatter. + +## Verification Plan + +### Manual Verification +1. **Re-run GitHub Action**: Trigger the workflow again on the existing PR. +2. **Check Logs**: Verify "OpenRouter response received" and no 404/401 errors. diff --git a/scripts/ai_reviewer.py b/scripts/ai_reviewer.py new file mode 100644 index 00000000..e2de9925 --- /dev/null +++ b/scripts/ai_reviewer.py @@ -0,0 +1,179 @@ +import os +import json +import logging +from github import Github +from openai import OpenAI + +# Configure Logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# Constants +IGNORED_EXTENSIONS = ['.json', '.md', '.txt', '.yml', '.yaml', '.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg'] +IGNORED_DIRS = ['dist', 'build', 'node_modules', '.github'] +MODEL_NAME = "mistralai/devstral-2512:free" + +def should_review(filename): + """Check if file should be reviewed based on extension and path.""" + if any(filename.endswith(ext) for ext in IGNORED_EXTENSIONS): + return False + if any(part in filename.split('/') for part in IGNORED_DIRS): + return False + return True + +def get_pr_diff(repo, pr_number): + """Fetch PR diff using PyGithub.""" + pr = repo.get_pull(pr_number) + files_data = [] + + for file in pr.get_files(): + if not should_review(file.filename): + continue + + # Only review added or modified files (not deleted) + if file.status == 'removed': + continue + + files_data.append({ + "filename": file.filename, + "patch": file.patch + }) + return pr, files_data + +def analyze_code_with_openrouter(files_data): + """Send code diff to OpenRouter for review.""" + api_key = os.getenv("OPENROUTER_API_KEY") + if not api_key: + logging.error("OPENROUTER_API_KEY not found in environment variables.") + return [] + + client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=api_key, + default_headers={ + "HTTP-Referer": "https://github.com/hapo-nghialuu/antigravity-kit", + "X-Title": "Antigravity AI Reviewer" + } + ) + + # Construct Prompt + # We ask for a strict JSON response. + prompt = """ + Bạn là một Senior Code Reviewer. Nhiệm vụ của bạn là review các đoạn code thay đổi trong Pull Request này. + + Hãy chỉ ra các vấn đề nghiêm trọng: + 1. Lỗi Logic (Logic Errors) - Rất quan trọng. + 2. Vấn đề Bảo mật (Security Vulnerabilities) - Rất quan trọng. + 3. Hiệu năng (Performance Issues). + 4. Code xấu, khó bảo trì (Bad Practices). + + Bỏ qua: + - Các lỗi format/style (đã có linter). + - Các thay đổi không quan trọng. + + Dữ liệu input là JSON list các file kèm patch. + Hãy trả về kết quả là một JSON list thuần túy (không markdown block, không giải thích thêm), mỗi item có format sau: + [ + { + "filename": "tên_file", + "line_number": số_dòng_trong_patch_để_comment, + "comment": "Nội dung review bằng tiếng Việt, ngắn gọn, súc tích." + } + ] + + Nếu code tốt hoàn toàn, hãy trả về danh sách rỗng []. + + CODE DIFF TO REVIEW: + """ + json.dumps(files_data) + + try: + response = client.chat.completions.create( + model=MODEL_NAME, + messages=[ + {"role": "system", "content": "You are a helpful code reviewer. Always respond with valid JSON code only. No markdown formatting."}, + {"role": "user", "content": prompt} + ], + # Note: response_format={"type": "json_object"} sometimes requires 'json' in prompt or specific model support. + # We trust the prompt instruction for now. + ) + + content = response.choices[0].message.content.strip() + + # Strip markdown code blocks if present (common issue with LLMs) + if content.startswith("```json"): + content = content[7:] + if content.startswith("```"): + content = content[3:] + if content.endswith("```"): + content = content[:-3] + + content = content.strip() + + logging.info("OpenRouter response received.") + return json.loads(content) + except json.JSONDecodeError: + logging.error(f"Failed to parse JSON response: {content}") + return [] + except Exception as e: + logging.error(f"Error calling OpenRouter: {e}") + return [] + +def post_comments(pr, comments): + """Post comments to the PR.""" + commit = pr.get_commits().reversed[0] # Get latest commit + + if not comments: + logging.info("No issues found. LGTM!") + return + + logging.info(f"Posting {len(comments)} comments...") + + for note in comments: + filename = note.get('filename') + line = note.get('line_number') # AI guess of the line + body = f"🤖 **AI Review**: {note.get('comment')}" + + try: + if not line: + pr.create_issue_comment(f"File `{filename}`: {body}") + continue + + pr.create_review_comment(body, commit, filename, line=int(line), side="RIGHT") + except Exception as e: + logging.warning(f"Failed to post comment on {filename}:{line}. Error: {e}") + pr.create_issue_comment(f"Could not comment on line {line} of {filename}. \n\n{body}") + +def main(): + github_token = os.getenv("GITHUB_TOKEN") + event_path = os.getenv("GITHUB_EVENT_PATH") + + if not github_token or not event_path: + logging.error("Missing GITHUB_TOKEN or GITHUB_EVENT_PATH.") + return + + with open(event_path, 'r') as f: + event_data = json.load(f) + + if 'pull_request' not in event_data: + logging.info("Not a pull_request event. Exiting.") + return + + pr_number = event_data['pull_request']['number'] + repo_name = event_data['repository']['full_name'] + + logging.info(f"Starting review for PR #{pr_number} in {repo_name}") + + g = Github(github_token) + repo = g.get_repo(repo_name) + pr, files_data = get_pr_diff(repo, pr_number) + + if not files_data: + logging.info("No reviewable files found.") + return + + logging.info(f"Analyzing {len(files_data)} files...") + comments = analyze_code_with_openrouter(files_data) + post_comments(pr, comments) + logging.info("Review complete.") + +if __name__ == "__main__": + main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000..af700426 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +PyGithub>=2.1.1 +openai>=1.55.0