diff --git a/.github/workflows/tabby-ai-review.yaml b/.github/workflows/tabby-ai-review.yaml new file mode 100644 index 00000000..b8d72b57 --- /dev/null +++ b/.github/workflows/tabby-ai-review.yaml @@ -0,0 +1,37 @@ +name: AI Code Review with Tabby + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write # 🔑 Needed to post PR comments + +jobs: + tabby-review: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # 🔁 Ensures full git history for diff + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dependencies + run: pip install -r ai-review/requirements.txt + + - name: Run Tabby PR review + env: + TABBY_URL: ${{ secrets.TABBY_URL }} + TABBY_USERNAME: ${{ secrets.TABBY_USERNAME }} + TABBY_PASSWORD: ${{ secrets.TABBY_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + run: python ai-review/review_bot.py diff --git a/ai-review/github_api.py b/ai-review/github_api.py new file mode 100644 index 00000000..6cc8a99f --- /dev/null +++ b/ai-review/github_api.py @@ -0,0 +1,17 @@ +import os +import requests + +def post_comment(file_path, comment): + repo = os.getenv("GITHUB_REPOSITORY") + pr_number = os.getenv("GITHUB_REF").split("/")[-1] + token = os.getenv("GITHUB_TOKEN") + + url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json" + } + + body = f"💡 **AI Review Suggestion for `{file_path}`**\n\n{comment}" + response = requests.post(url, json={"body": body}, headers=headers) + response.raise_for_status() diff --git a/ai-review/requirements.txt b/ai-review/requirements.txt new file mode 100644 index 00000000..f79c0da7 --- /dev/null +++ b/ai-review/requirements.txt @@ -0,0 +1,2 @@ +requests +rich diff --git a/ai-review/review_bot.py b/ai-review/review_bot.py new file mode 100644 index 00000000..cb2d8b61 --- /dev/null +++ b/ai-review/review_bot.py @@ -0,0 +1,69 @@ +import os +import requests +from tabby_client import get_tabby_review + +# --- Config --- +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") # e.g., "myuser/myrepo" +GITHUB_REF = os.getenv("GITHUB_REF", "") # e.g., "refs/pull/42/merge" + +def get_pull_request_number(): + """ + Extract the pull request number from GITHUB_REF (e.g. "refs/pull/42/merge"). + """ + try: + return GITHUB_REF.split("/")[2] + except IndexError: + raise RuntimeError(f"Cannot extract PR number from GITHUB_REF='{GITHUB_REF}'") + +def get_changed_files(pr_number): + """ + Fetch the list of changed files in the PR using GitHub API. + """ + url = f"https://api.github.com/repos/{GITHUB_REPOSITORY}/pulls/{pr_number}/files" + headers = {"Authorization": f"Bearer {GITHUB_TOKEN}"} + response = requests.get(url, headers=headers) + response.raise_for_status() + files = response.json() + return [f["filename"] for f in files if f["filename"].endswith((".py", ".js", ".ts", ".java", ".go", ".rb"))] + +def post_comment(pr_number, body): + """ + Post a comment on the pull request. + """ + url = f"https://api.github.com/repos/{GITHUB_REPOSITORY}/issues/{pr_number}/comments" + headers = { + "Authorization": f"Bearer {GITHUB_TOKEN}", + "Accept": "application/vnd.github+json" + } + response = requests.post(url, headers=headers, json={"body": body}) + response.raise_for_status() + +def main(): + pr_number = get_pull_request_number() + print(f"🔍 Pull Request #{pr_number}") + + changed_files = get_changed_files(pr_number) + if not changed_files: + print("✅ No code files changed. Skipping review.") + return + + print(f"📂 Changed files: {changed_files}") + + for file_path in changed_files: + try: + with open(file_path, "r", encoding="utf-8") as f: + code = f.read() + + prompt = f"Review this code and suggest improvements:\n\n{code}" + suggestion = get_tabby_review(prompt) + + comment_body = f"💡 **AI Review Suggestion for `{file_path}`**\n\n{suggestion}" + post_comment(pr_number, comment_body) + print(f"✅ Comment posted for {file_path}") + + except Exception as e: + print(f"⚠️ Skipping `{file_path}` due to error: {e}") + +if __name__ == "__main__": + main() diff --git a/ai-review/tabby_client.py b/ai-review/tabby_client.py new file mode 100644 index 00000000..fbb0f523 --- /dev/null +++ b/ai-review/tabby_client.py @@ -0,0 +1,40 @@ +import json +import requests + +TABBY_URL = "http://54.196.243.3:8080" +TABBY_AUTH_TOKEN = "auth_9675f108605f42f8bad46e5324d756ab" # <-- your manual token + +def get_tabby_review(prompt: str) -> str: + headers = { + "Authorization": f"Bearer {TABBY_AUTH_TOKEN}", + "Content-Type": "application/json" + } + + payload = { + "model": "Qwen2-1.5B-Instruct", + "messages": [{"role": "user", "content": prompt}], + "stream": True + } + + response = requests.post(f"{TABBY_URL}/v1/chat/completions", + headers=headers, + json=payload, + stream=True) + + if response.status_code != 200: + raise RuntimeError(f"Tabby Error: {response.status_code} - {response.text}") + + result = "" + for line in response.iter_lines(decode_unicode=True): + if line and line.startswith("data: "): + chunk = line.removeprefix("data: ").strip() + if chunk == "[DONE]": + break + try: + json_chunk = json.loads(chunk) + delta = json_chunk["choices"][0]["delta"] + result += delta.get("content", "") + except Exception as e: + print(f"⚠️ Error parsing chunk: {e}") + + return result.strip()