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..8f46eada --- /dev/null +++ b/ai-review/review_bot.py @@ -0,0 +1,75 @@ +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" +TABBY_URL = os.getenv("TABBY_URL", "http://54.196.243.3:8080") + + +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, tabby_url=TABBY_URL) + + 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 `{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..f88bcdfd --- /dev/null +++ b/ai-review/tabby_client.py @@ -0,0 +1,29 @@ +import requests +import os +from requests.auth import HTTPBasicAuth + +def get_tabby_review(prompt: str, tabby_url=None) -> str: + tabby_url = tabby_url or os.getenv("TABBY_URL", "http://54.196.243.3:8080") + username = os.getenv("TABBY_USERNAME") + password = os.getenv("TABBY_PASSWORD") + + if not username or not password: + print("⚠️ Missing TabbyML credentials. Set TABBY_USERNAME and TABBY_PASSWORD.") + return "⚠️ Error: Missing credentials for TabbyML." + + try: + response = requests.post( + f"{tabby_url}/v1/chat/completions", + json={ + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.4, + }, + timeout=30, + auth=HTTPBasicAuth(username, password) + ) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + + except requests.exceptions.RequestException as e: + print(f"Error: Failed to communicate with TabbyML: {e}") + return "⚠️ Error: Unable to connect to TabbyML server."