Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/tabby-ai-review.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions ai-review/github_api.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions ai-review/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
rich
75 changes: 75 additions & 0 deletions ai-review/review_bot.py
Original file line number Diff line number Diff line change
@@ -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()
29 changes: 29 additions & 0 deletions ai-review/tabby_client.py
Original file line number Diff line number Diff line change
@@ -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."
Loading