123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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')
- GITHUB_REPOSITORY = os.getenv('GITHUB_REPOSITORY')
- G4F_PROVIDER = os.getenv('G4F_PROVIDER')
- G4F_MODEL = os.getenv('G4F_MODEL') or g4f.models.gpt_4
- def get_pr_details(github: Github) -> PullRequest:
- """
- Retrieves the details of the pull request from GitHub.
- Args:
- github (Github): The Github object to interact with the GitHub API.
- Returns:
- PullRequest: An object representing the pull request.
- """
- with open('./pr_number', 'r') as file:
- pr_number = file.read().strip()
- if not pr_number:
- return
- repo = github.get_repo(GITHUB_REPOSITORY)
- pull = repo.get_pull(int(pr_number))
- return pull
- def get_diff(diff_url: str) -> str:
- """
- Fetches the diff of the pull request from a given URL.
- Args:
- diff_url (str): URL to the pull request diff.
- Returns:
- str: The diff of the pull request.
- """
- response = requests.get(diff_url)
- response.raise_for_status()
- return response.text
- def read_json(text: str) -> dict:
- """
- Parses JSON code block from a string.
- Args:
- text (str): A string containing a JSON code block.
- Returns:
- dict: A dictionary parsed from the JSON code block.
- """
- 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) -> str:
- """
- Extracts text from a markdown code block.
- Args:
- text (str): A string containing a markdown code block.
- Returns:
- str: The extracted text.
- """
- 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: str, 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.
- as_json (bool): Whether to parse the response as JSON.
- Returns:
- Union[dict, str]: The parsed response from g4f, either as a dictionary or a string.
- """
- response = g4f.ChatCompletion.create(
- G4F_MODEL,
- [{'role': 'user', 'content': prompt}],
- G4F_PROVIDER,
- ignore_stream_and_auth=True
- )
- return read_json(response) if as_json else read_text(response)
- def analyze_code(pull: PullRequest, diff: str)-> list[dict]:
- """
- Analyzes the code changes in the pull request.
- Args:
- pull (PullRequest): The pull request object.
- diff (str): The diff of the pull request.
- Returns:
- list[dict]: A 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:]
- changed_lines = []
- 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')) and changed_lines:
- prompt = create_analyze_prompt(changed_lines, pull, current_file_path)
- response = get_ai_response(prompt)
- for review in response.get('reviews', []):
- review['path'] = current_file_path
- comments.append(review)
- current_file_path = None
- elif line.startswith('-'):
- changed_lines.append(line)
- else:
- changed_lines.append(f"{offset_line}:{line}")
- offset_line += 1
-
- return comments
- def create_analyze_prompt(changed_lines: list[str], pull: PullRequest, file_path: str):
- """
- Creates a prompt for the g4f model.
- Args:
- changed_lines (list[str]): The lines of code that have changed.
- pull (PullRequest): The pull request object.
- file_path (str): The path to the file being reviewed.
- Returns:
- str: The generated prompt.
- """
- code = "\n".join(changed_lines)
- 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 comment.
- Args:
- pull (PullRequest): The pull request object.
- diff (str): The diff of the pull request.
- Returns:
- str: The generated prompt for review.
- """
- return f"""Your task is to review a pull request. Instructions:
- - Write in name of g4f copilot. Don't use placeholder.
- - Write the review in GitHub Markdown format.
- - Thank the author for contributing to the project.
- 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)
- if not pull:
- print(f"No PR number found")
- exit()
- if pull.get_reviews().totalCount > 0 or pull.get_issue_comments().totalCount > 0:
- print(f"Has already a review")
- exit()
- diff = get_diff(pull.diff_url)
- except Exception as e:
- print(f"Error get details: {e.__class__.__name__}: {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:
- if comments:
- pull.create_review(body=review, comments=comments)
- else:
- pull.create_issue_comment(body=review)
- except Exception as e:
- print(f"Error posting review: {e}")
- exit(1)
- if __name__ == "__main__":
- main()
|