From 7f4d9ba82b73a04481a4ee52bf150f9cc68a2c07 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Sat, 20 Jan 2024 18:16:18 +0100 Subject: Add copilot github action --- .github/workflows/copilot.yml | 33 +++++++ etc/tool/copilot.py | 215 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 .github/workflows/copilot.yml create mode 100644 etc/tool/copilot.py diff --git a/.github/workflows/copilot.yml b/.github/workflows/copilot.yml new file mode 100644 index 00000000..38c24378 --- /dev/null +++ b/.github/workflows/copilot.yml @@ -0,0 +1,33 @@ +name: AI Code Reviewer + +on: + pull_request: + types: + - opened + - synchronize + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: 'pip' + - name: Install Requirements + run: pip install -r requirements.txt + - name: Install PyGithub + run: pip install PyGithub + - name: AI Code Review + run: python -m etc.tool.copilot \ No newline at end of file diff --git a/etc/tool/copilot.py b/etc/tool/copilot.py new file mode 100644 index 00000000..62698c70 --- /dev/null +++ b/etc/tool/copilot.py @@ -0,0 +1,215 @@ +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent.parent)) + +import g4f +import json +import os +import re +import requests +from typing import Union +from github import Github +from github.PullRequest import PullRequest + +g4f.debug.logging = True +g4f.debug.version_check = False + +GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') +G4F_PROVIDER = os.getenv('G4F_PROVIDER') or g4f.Provider.OpenaiChat +G4F_MODEL = os.getenv('G4F_MODEL') or g4f.models.gpt_4 + +def get_pr_details(github: Github) -> PullRequest: + """ + Rteurns the details of the pull request from GitHub. + + Returns: + PullRequest: A PullRequest instance. + """ + with open(os.getenv('GITHUB_EVENT_PATH', ''), 'r') as file: + data = json.load(file) + + repo = github.get_repo(f"{data['repository']['owner']['login']}/{data['repository']['name']}") + pull = repo.get_pull(data['number']) + + return pull + +def get_diff(diff_url: str) -> str: + """ + Fetches the diff of the pull request. + + Args: + pull (PullRequest): Pull request. + + Returns: + str or None: The diff of the pull request or None if not available. + """ + response = requests.get(diff_url) + response.raise_for_status() + return response.text + +def read_json(text: str) -> dict: + match = re.search(r"```(json|)\n(?P[\S\s]+?)\n```", text) + if match: + text = match.group("code") + try: + return json.loads(text.strip()) + except json.JSONDecodeError: + print("No valid json:", text) + return {} + +def read_text(text: str) -> dict: + match = re.search(r"```(markdown|)\n(?P[\S\s]+?)\n```", text) + if match: + return match.group("text") + return text + +def get_ai_response(prompt, as_json: bool = True) -> Union[dict, str]: + """ + Gets a response from g4f API based on the prompt. + + Args: + prompt (str): The prompt to send to g4f. + + Returns: + dict: The parsed response from g4f. + """ + response = g4f.ChatCompletion.create( + G4F_MODEL, + [{'role': 'user', 'content': prompt}], + G4F_PROVIDER, + ignore_stream_and_auth=True + ) + if as_json: + return read_json(response) + return read_text(response) + +def analyze_code(pull: PullRequest, diff: str)-> list: + """ + Analyzes the code changes in the pull request. + + Args: + diff (str): The diff of the pull request. + pr_details (dict): Details of the pull request. + + Returns: + list: List of comments generated by the analysis. + """ + comments = [] + changed_lines = [] + current_file_path = None + offset_line = 0 + + for line in diff.split('\n'): + if line.startswith('+++ b/'): + current_file_path = line[6:] + elif line.startswith('@@'): + match = re.search(r'\+([0-9]+?),', line) + if match: + offset_line = int(match.group(1)) + elif current_file_path: + if line.startswith('\\') or line.startswith('diff'): + prompt = create_prompt(changed_lines, pull, current_file_path, offset_line) + response = get_ai_response(prompt) + for review in response.get('reviews', []): + review['path'] = current_file_path + comments.append(review) + changed_lines = [] + current_file_path = None + elif not line.startswith('-'): + changed_lines.append(line) + + return comments + +def create_prompt(changed_lines: list, pull: PullRequest, file_path: str, offset_line: int): + """ + Creates a prompt for the g4f model. + + Args: + diff (str): The line of code to analyze. + pr_details (dict): Details of the pull request. + + Returns: + str: The generated prompt. + """ + code = "\n".join([f"{offset_line+idx}:{line}" for idx, line in enumerate(changed_lines)]) + print("Code:", code) + example = '{"reviews": [{"line": , "body": ""}]}' + return f"""Your task is to review pull requests. Instructions: +- Provide the response in following JSON format: {example} +- Do not give positive comments or compliments. +- Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array. +- Write the comment in GitHub Markdown format. +- Use the given description only for the overall context and only comment the code. +- IMPORTANT: NEVER suggest adding comments to the code. + +Review the following code diff in the file "{file_path}" and take the pull request title and description into account when writing the response. + +Pull request title: {pull.title} +Pull request description: +--- +{pull.body} +--- + +Each line is prefixed by its number. Code to review: +``` +{code} +``` +""" + +def create_review_prompt(pull: PullRequest, diff: str): + """ + Creates a prompt to create a review. + + Args: + diff (str): The line of code to analyze. + + Returns: + str: The generated prompt. + """ + return f"""Your task is to review a pull request. Instructions: +- Your name / you are copilot. +- Write the review in GitHub Markdown format. +- Thank the author for contributing to the project. +- Point out that you might leave a few comments on the files. + +Pull request author: {pull.user.name} +Pull request title: {pull.title} +Pull request description: +--- +{pull.body} +--- + +Diff: +```diff +{diff} +``` +""" + +def main(): + try: + github = Github(GITHUB_TOKEN) + pull = get_pr_details(github) + diff = get_diff(pull.diff_url) + except Exception as e: + print(f"Error get details: {e}") + exit(1) + try: + review = get_ai_response(create_review_prompt(pull, diff), False) + except Exception as e: + print(f"Error create review: {e}") + exit(1) + try: + comments = analyze_code(pull, diff) + except Exception as e: + print(f"Error analyze: {e}") + exit(1) + print("Comments:", comments) + try: + pull.create_review(body=review, comments=comments) + except Exception as e: + print(f"Error posting review: {e}") + exit(1) + +if __name__ == "__main__": + main() -- cgit v1.2.3