summaryrefslogblamecommitdiffstats
path: root/etc/tool/copilot.py
blob: 62698c7080aa1bdd36e0281afecd69d4f26922bb (plain) (tree)






















































































































































































































                                                                                                                                                
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<code>[\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<text>[\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": <line_number>, "body": "<review comment>"}]}'
    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()