copilot.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import sys
  2. from pathlib import Path
  3. sys.path.append(str(Path(__file__).parent.parent.parent))
  4. import g4f
  5. import json
  6. import os
  7. import re
  8. import requests
  9. from typing import Union
  10. from github import Github
  11. from github.PullRequest import PullRequest
  12. g4f.debug.logging = True
  13. g4f.debug.version_check = False
  14. GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
  15. GITHUB_REPOSITORY = os.getenv('GITHUB_REPOSITORY')
  16. G4F_PROVIDER = os.getenv('G4F_PROVIDER')
  17. G4F_MODEL = os.getenv('G4F_MODEL') or g4f.models.gpt_4
  18. def get_pr_details(github: Github) -> PullRequest:
  19. """
  20. Retrieves the details of the pull request from GitHub.
  21. Args:
  22. github (Github): The Github object to interact with the GitHub API.
  23. Returns:
  24. PullRequest: An object representing the pull request.
  25. """
  26. with open('./pr_number', 'r') as file:
  27. pr_number = file.read().strip()
  28. if not pr_number:
  29. return
  30. repo = github.get_repo(GITHUB_REPOSITORY)
  31. pull = repo.get_pull(int(pr_number))
  32. return pull
  33. def get_diff(diff_url: str) -> str:
  34. """
  35. Fetches the diff of the pull request from a given URL.
  36. Args:
  37. diff_url (str): URL to the pull request diff.
  38. Returns:
  39. str: The diff of the pull request.
  40. """
  41. response = requests.get(diff_url)
  42. response.raise_for_status()
  43. return response.text
  44. def read_json(text: str) -> dict:
  45. """
  46. Parses JSON code block from a string.
  47. Args:
  48. text (str): A string containing a JSON code block.
  49. Returns:
  50. dict: A dictionary parsed from the JSON code block.
  51. """
  52. match = re.search(r"```(json|)\n(?P<code>[\S\s]+?)\n```", text)
  53. if match:
  54. text = match.group("code")
  55. try:
  56. return json.loads(text.strip())
  57. except json.JSONDecodeError:
  58. print("No valid json:", text)
  59. return {}
  60. def read_text(text: str) -> str:
  61. """
  62. Extracts text from a markdown code block.
  63. Args:
  64. text (str): A string containing a markdown code block.
  65. Returns:
  66. str: The extracted text.
  67. """
  68. match = re.search(r"```(markdown|)\n(?P<text>[\S\s]+?)\n```", text)
  69. if match:
  70. return match.group("text")
  71. return text
  72. def get_ai_response(prompt: str, as_json: bool = True) -> Union[dict, str]:
  73. """
  74. Gets a response from g4f API based on the prompt.
  75. Args:
  76. prompt (str): The prompt to send to g4f.
  77. as_json (bool): Whether to parse the response as JSON.
  78. Returns:
  79. Union[dict, str]: The parsed response from g4f, either as a dictionary or a string.
  80. """
  81. response = g4f.ChatCompletion.create(
  82. G4F_MODEL,
  83. [{'role': 'user', 'content': prompt}],
  84. G4F_PROVIDER,
  85. ignore_stream_and_auth=True
  86. )
  87. return read_json(response) if as_json else read_text(response)
  88. def analyze_code(pull: PullRequest, diff: str)-> list[dict]:
  89. """
  90. Analyzes the code changes in the pull request.
  91. Args:
  92. pull (PullRequest): The pull request object.
  93. diff (str): The diff of the pull request.
  94. Returns:
  95. list[dict]: A list of comments generated by the analysis.
  96. """
  97. comments = []
  98. changed_lines = []
  99. current_file_path = None
  100. offset_line = 0
  101. for line in diff.split('\n'):
  102. if line.startswith('+++ b/'):
  103. current_file_path = line[6:]
  104. changed_lines = []
  105. elif line.startswith('@@'):
  106. match = re.search(r'\+([0-9]+?),', line)
  107. if match:
  108. offset_line = int(match.group(1))
  109. elif current_file_path:
  110. if (line.startswith('\\') or line.startswith('diff')) and changed_lines:
  111. prompt = create_analyze_prompt(changed_lines, pull, current_file_path)
  112. response = get_ai_response(prompt)
  113. for review in response.get('reviews', []):
  114. review['path'] = current_file_path
  115. comments.append(review)
  116. current_file_path = None
  117. elif line.startswith('-'):
  118. changed_lines.append(line)
  119. else:
  120. changed_lines.append(f"{offset_line}:{line}")
  121. offset_line += 1
  122. return comments
  123. def create_analyze_prompt(changed_lines: list[str], pull: PullRequest, file_path: str):
  124. """
  125. Creates a prompt for the g4f model.
  126. Args:
  127. changed_lines (list[str]): The lines of code that have changed.
  128. pull (PullRequest): The pull request object.
  129. file_path (str): The path to the file being reviewed.
  130. Returns:
  131. str: The generated prompt.
  132. """
  133. code = "\n".join(changed_lines)
  134. example = '{"reviews": [{"line": <line_number>, "body": "<review comment>"}]}'
  135. return f"""Your task is to review pull requests. Instructions:
  136. - Provide the response in following JSON format: {example}
  137. - Do not give positive comments or compliments.
  138. - Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array.
  139. - Write the comment in GitHub Markdown format.
  140. - Use the given description only for the overall context and only comment the code.
  141. - IMPORTANT: NEVER suggest adding comments to the code.
  142. Review the following code diff in the file "{file_path}" and take the pull request title and description into account when writing the response.
  143. Pull request title: {pull.title}
  144. Pull request description:
  145. ---
  146. {pull.body}
  147. ---
  148. Each line is prefixed by its number. Code to review:
  149. ```
  150. {code}
  151. ```
  152. """
  153. def create_review_prompt(pull: PullRequest, diff: str):
  154. """
  155. Creates a prompt to create a review comment.
  156. Args:
  157. pull (PullRequest): The pull request object.
  158. diff (str): The diff of the pull request.
  159. Returns:
  160. str: The generated prompt for review.
  161. """
  162. return f"""Your task is to review a pull request. Instructions:
  163. - Write in name of g4f copilot. Don't use placeholder.
  164. - Write the review in GitHub Markdown format.
  165. - Thank the author for contributing to the project.
  166. Pull request author: {pull.user.name}
  167. Pull request title: {pull.title}
  168. Pull request description:
  169. ---
  170. {pull.body}
  171. ---
  172. Diff:
  173. ```diff
  174. {diff}
  175. ```
  176. """
  177. def main():
  178. try:
  179. github = Github(GITHUB_TOKEN)
  180. pull = get_pr_details(github)
  181. if not pull:
  182. print(f"No PR number found")
  183. exit()
  184. if pull.get_reviews().totalCount > 0 or pull.get_issue_comments().totalCount > 0:
  185. print(f"Has already a review")
  186. exit()
  187. diff = get_diff(pull.diff_url)
  188. except Exception as e:
  189. print(f"Error get details: {e.__class__.__name__}: {e}")
  190. exit(1)
  191. try:
  192. review = get_ai_response(create_review_prompt(pull, diff), False)
  193. except Exception as e:
  194. print(f"Error create review: {e}")
  195. exit(1)
  196. try:
  197. comments = analyze_code(pull, diff)
  198. except Exception as e:
  199. print(f"Error analyze: {e}")
  200. exit(1)
  201. print("Comments:", comments)
  202. try:
  203. if comments:
  204. pull.create_review(body=review, comments=comments)
  205. else:
  206. pull.create_issue_comment(body=review)
  207. except Exception as e:
  208. print(f"Error posting review: {e}")
  209. exit(1)
  210. if __name__ == "__main__":
  211. main()