diff --git a/.github/mode-context.json b/.github/mode-context.json new file mode 100644 index 0000000..3d0ee49 --- /dev/null +++ b/.github/mode-context.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/github/context-server/main/schema.json", + "name": "LLM Code Review Bot", + "description": "A bot that uses LLMs to review pull request code diffs and post comments.", + "app": { + "name": "llm-code-review-bot", + "version": "0.1.0" + }, + "config": { + "entrypoint": "reviewbot.main:main", + "runtime": { + "language": "python" + } + }, + "capabilities": { + "pullRequestReview": true + } +} diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 0000000..576479e --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,36 @@ +name: LLM Code Review + +on: + pull_request: + types: [opened, synchronize] + +jobs: + review: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip install -r requirements.txt + + - name: Run LLM code review + env: + GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYTHONPATH: ${{ github.workspace }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REF: ${{ github.ref }} + run: | + python3 -m reviewbot.main diff --git a/README.md b/README.md new file mode 100644 index 0000000..830f5a5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# LLM Code Review Bot \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..112eec9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +certifi==2025.4.26 +cffi==1.16.0 +charset-normalizer==3.4.2 +cryptography==41.0.5 +greenlet==1.1.2 +idna==3.10 +msgpack==1.0.2 +pycparser==2.21 +pycryptodome==3.19.0 +pynvim==0.4.3 +requests==2.32.3 +urllib3==2.4.0 diff --git a/reviewbot/__pycache__/main.cpython-310.pyc b/reviewbot/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..e9b088c Binary files /dev/null and b/reviewbot/__pycache__/main.cpython-310.pyc differ diff --git a/reviewbot/__pycache__/utils.cpython-310.pyc b/reviewbot/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..446cae4 Binary files /dev/null and b/reviewbot/__pycache__/utils.cpython-310.pyc differ diff --git a/reviewbot/main.py b/reviewbot/main.py new file mode 100644 index 0000000..29ecb8d --- /dev/null +++ b/reviewbot/main.py @@ -0,0 +1,34 @@ +import os +import requests +from reviewbot.utils import get_diff, post_pr_comment, get_pr_number + +llm_prompt = "Please reviiew thsi code diff, list improvements and potential optimizations in bullet points"; + +def review_with_groq(diff): + api_key = os.environ.get("GROQ_API_KEY") + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + payload = { + "model": "meta-llama/llama-4-scout-17b-16e-instruct", + "messages": [ + {"role": "system", "content": "You are a senior software engineer doing code reviews."}, + {"role": "user", "content": f"{llm_prompt} given the following diff{diff}"} + ], + "temperature": 0.3, + } + + response = requests.post("https://api.groq.com/openai/v1/chat/completions", headers=headers, json=payload) + + print("Raw Groq response:", response.status_code, response.text) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + +def main(): + diff = get_diff() + review = review_with_groq(diff) + post_pr_comment(review) + +if __name__ == "__main__": + main() diff --git a/reviewbot/utils.py b/reviewbot/utils.py new file mode 100644 index 0000000..3d7249c --- /dev/null +++ b/reviewbot/utils.py @@ -0,0 +1,64 @@ +import os +import requests +import json + +def get_pr_number(): + event_path = os.getenv("GITHUB_EVENT_PATH") + if not event_path: + raise Exception("GITHUB_EVENT_PATH not set") + + with open(event_path, 'r') as f: + event = json.load(f) + + return event["pull_request"]["number"] + +def get_diff(): + repo = os.environ.get("GITHUB_REPOSITORY") + pr_number = get_pr_number() + + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise Exception("Missing GITHUB_TOKEN environment variable.") + + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github.v3+json", + } + + url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files" + + diffs = [] + page = 1 + while True: + response = requests.get(url, headers=headers, params={"page": page, "per_page": 100}) + response.raise_for_status() + files = response.json() + if not files: + break + + for f in files: + patch = f.get("patch") + if patch: + diffs.append(f"File: {f['filename']}\n{patch}") + + page += 1 + + return "\n\n".join(diffs) + +def post_pr_comment(body: str): + repo = os.getenv("GITHUB_REPOSITORY") + token = os.getenv("GITHUB_TOKEN") + pr_number = get_pr_number() + + url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json" + } + payload = { + "body": body + } + + response = requests.post(url, headers=headers, json=payload) + print("GitHub API response:", response.status_code, response.text) + response.raise_for_status() diff --git a/test_module/hello.py b/test_module/hello.py new file mode 100644 index 0000000..326fe9f --- /dev/null +++ b/test_module/hello.py @@ -0,0 +1,11 @@ +# Hello.py: A sample python script used for testing reviews + +def twoSum(array, targetSum): + for i in range(0, len(array)): + for j in range(i+1, len(array)): + if array[i] + array[j] == targetSum: + return ([array[i], array[j]]) + return [] + +twoSum([2,7,11,15], 9) +