summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/copilot.yml31
-rw-r--r--etc/tool/copilot.py215
-rw-r--r--etc/unittest/main.py9
-rw-r--r--g4f/Provider/base_provider.py30
-rw-r--r--g4f/errors.py3
-rw-r--r--g4f/webdriver.py4
6 files changed, 278 insertions, 14 deletions
diff --git a/.github/workflows/copilot.yml b/.github/workflows/copilot.yml
new file mode 100644
index 00000000..c34dcb11
--- /dev/null
+++ b/.github/workflows/copilot.yml
@@ -0,0 +1,31 @@
+name: AI Code Reviewer
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+permissions: write-all
+
+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..9be1291c
--- /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')
+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') and changed_lines:
+ prompt = create_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)
+ changed_lines = []
+ current_file_path = None
+ elif not line.startswith('-'):
+ changed_lines.append(f"{offset_line}:{line}")
+ offset_line += 1
+
+ return comments
+
+def create_prompt(changed_lines: list, pull: PullRequest, file_path: str):
+ """
+ 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(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.
+
+ 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()
diff --git a/etc/unittest/main.py b/etc/unittest/main.py
index 4b92a6a6..ad1fe02d 100644
--- a/etc/unittest/main.py
+++ b/etc/unittest/main.py
@@ -39,10 +39,11 @@ class TestBackendApi(unittest.TestCase):
class TestChatCompletion(unittest.TestCase):
- def test_create(self):
+ def test_create_default(self):
messages = [{'role': 'user', 'content': 'Hello'}]
result = ChatCompletion.create(g4f.models.default, messages)
- self.assertTrue("Hello" in result or "Good" in result)
+ if "Good" not in result and "Hi" not in result:
+ self.assertIn("Hello", result)
def test_get_last_provider(self):
messages = [{'role': 'user', 'content': 'Hello'}]
@@ -53,14 +54,14 @@ class TestChatCompletion(unittest.TestCase):
messages = [{'role': 'user', 'content': 'Hello'}]
provider = g4f.Provider.Bing
result = ChatCompletion.create(g4f.models.default, messages, provider)
- self.assertTrue("Bing" in result)
+ self.assertIn("Bing", result)
class TestChatCompletionAsync(unittest.IsolatedAsyncioTestCase):
async def test_async(self):
messages = [{'role': 'user', 'content': 'Hello'}]
result = await ChatCompletion.create_async(g4f.models.default, messages, MockProvider)
- self.assertTrue("Mock" in result)
+ self.assertEqual("Mock", result)
class TestUtilityFunctions(unittest.TestCase):
diff --git a/g4f/Provider/base_provider.py b/g4f/Provider/base_provider.py
index fd92d17a..95f1b0b2 100644
--- a/g4f/Provider/base_provider.py
+++ b/g4f/Provider/base_provider.py
@@ -8,6 +8,7 @@ from inspect import signature, Parameter
from .helper import get_event_loop, get_cookies, format_prompt
from ..typing import CreateResult, AsyncResult, Messages
from ..base_provider import BaseProvider
+from ..errors import NestAsyncioError
if sys.version_info < (3, 10):
NoneType = type(None)
@@ -48,7 +49,7 @@ class AbstractProvider(BaseProvider):
Returns:
str: The created result as a string.
"""
- loop = loop or get_event_loop()
+ loop = loop or asyncio.get_running_loop()
def create_func() -> str:
return "".join(cls.create_completion(model, messages, False, **kwargs))
@@ -101,8 +102,6 @@ class AsyncProvider(AbstractProvider):
model: str,
messages: Messages,
stream: bool = False,
- *,
- loop: AbstractEventLoop = None,
**kwargs
) -> CreateResult:
"""
@@ -119,9 +118,15 @@ class AsyncProvider(AbstractProvider):
Returns:
CreateResult: The result of the completion creation.
"""
- loop = loop or get_event_loop()
- coro = cls.create_async(model, messages, **kwargs)
- yield loop.run_until_complete(coro)
+ try:
+ loop = asyncio.get_running_loop()
+ if not hasattr(loop.__class__, "_nest_patched"):
+ raise NestAsyncioError(
+ 'Use "create_async" instead of "create" function in a running event loop. Or use "nest_asyncio" package.'
+ )
+ except RuntimeError:
+ pass
+ yield asyncio.run(cls.create_async(model, messages, **kwargs))
@staticmethod
@abstractmethod
@@ -159,8 +164,6 @@ class AsyncGeneratorProvider(AsyncProvider):
model: str,
messages: Messages,
stream: bool = True,
- *,
- loop: AbstractEventLoop = None,
**kwargs
) -> CreateResult:
"""
@@ -177,7 +180,16 @@ class AsyncGeneratorProvider(AsyncProvider):
Returns:
CreateResult: The result of the streaming completion creation.
"""
- loop = loop or get_event_loop()
+ try:
+ loop = asyncio.get_running_loop()
+ if not hasattr(loop.__class__, "_nest_patched"):
+ raise NestAsyncioError(
+ 'Use "create_async" instead of "create" function in a running event loop. Or use "nest_asyncio" package.'
+ )
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+
generator = cls.create_async_generator(model, messages, stream=stream, **kwargs)
gen = generator.__aiter__()
diff --git a/g4f/errors.py b/g4f/errors.py
index b874435a..c0e6ddfc 100644
--- a/g4f/errors.py
+++ b/g4f/errors.py
@@ -23,4 +23,7 @@ class RetryNoProviderError(Exception):
pass
class VersionNotFoundError(Exception):
+ pass
+
+class NestAsyncioError(Exception):
pass \ No newline at end of file
diff --git a/g4f/webdriver.py b/g4f/webdriver.py
index 9a83215f..85d6d695 100644
--- a/g4f/webdriver.py
+++ b/g4f/webdriver.py
@@ -6,6 +6,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from os import path
+from os import access, R_OK
from . import debug
try:
@@ -40,8 +41,9 @@ def get_browser(
options = ChromeOptions()
if proxy:
options.add_argument(f'--proxy-server={proxy}')
+ # Check for system driver in docker
driver = '/usr/bin/chromedriver'
- if not path.isfile(driver):
+ if not path.isfile(driver) or not access(driver, R_OK):
driver = None
return Chrome(
options=options,