mirror of
https://github.com/xtekky/gpt4free.git
synced 2024-11-27 05:25:33 +03:00
commit
a98a9e92bc
31
.github/workflows/copilot.yml
vendored
Normal file
31
.github/workflows/copilot.yml
vendored
Normal file
@ -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
|
215
etc/tool/copilot.py
Normal file
215
etc/tool/copilot.py
Normal file
@ -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()
|
@ -39,10 +39,11 @@ class TestBackendApi(unittest.TestCase):
|
|||||||
|
|
||||||
class TestChatCompletion(unittest.TestCase):
|
class TestChatCompletion(unittest.TestCase):
|
||||||
|
|
||||||
def test_create(self):
|
def test_create_default(self):
|
||||||
messages = [{'role': 'user', 'content': 'Hello'}]
|
messages = [{'role': 'user', 'content': 'Hello'}]
|
||||||
result = ChatCompletion.create(g4f.models.default, messages)
|
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):
|
def test_get_last_provider(self):
|
||||||
messages = [{'role': 'user', 'content': 'Hello'}]
|
messages = [{'role': 'user', 'content': 'Hello'}]
|
||||||
@ -53,14 +54,14 @@ class TestChatCompletion(unittest.TestCase):
|
|||||||
messages = [{'role': 'user', 'content': 'Hello'}]
|
messages = [{'role': 'user', 'content': 'Hello'}]
|
||||||
provider = g4f.Provider.Bing
|
provider = g4f.Provider.Bing
|
||||||
result = ChatCompletion.create(g4f.models.default, messages, provider)
|
result = ChatCompletion.create(g4f.models.default, messages, provider)
|
||||||
self.assertTrue("Bing" in result)
|
self.assertIn("Bing", result)
|
||||||
|
|
||||||
class TestChatCompletionAsync(unittest.IsolatedAsyncioTestCase):
|
class TestChatCompletionAsync(unittest.IsolatedAsyncioTestCase):
|
||||||
|
|
||||||
async def test_async(self):
|
async def test_async(self):
|
||||||
messages = [{'role': 'user', 'content': 'Hello'}]
|
messages = [{'role': 'user', 'content': 'Hello'}]
|
||||||
result = await ChatCompletion.create_async(g4f.models.default, messages, MockProvider)
|
result = await ChatCompletion.create_async(g4f.models.default, messages, MockProvider)
|
||||||
self.assertTrue("Mock" in result)
|
self.assertEqual("Mock", result)
|
||||||
|
|
||||||
class TestUtilityFunctions(unittest.TestCase):
|
class TestUtilityFunctions(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from inspect import signature, Parameter
|
|||||||
from .helper import get_event_loop, get_cookies, format_prompt
|
from .helper import get_event_loop, get_cookies, format_prompt
|
||||||
from ..typing import CreateResult, AsyncResult, Messages
|
from ..typing import CreateResult, AsyncResult, Messages
|
||||||
from ..base_provider import BaseProvider
|
from ..base_provider import BaseProvider
|
||||||
|
from ..errors import NestAsyncioError
|
||||||
|
|
||||||
if sys.version_info < (3, 10):
|
if sys.version_info < (3, 10):
|
||||||
NoneType = type(None)
|
NoneType = type(None)
|
||||||
@ -48,7 +49,7 @@ class AbstractProvider(BaseProvider):
|
|||||||
Returns:
|
Returns:
|
||||||
str: The created result as a string.
|
str: The created result as a string.
|
||||||
"""
|
"""
|
||||||
loop = loop or get_event_loop()
|
loop = loop or asyncio.get_running_loop()
|
||||||
|
|
||||||
def create_func() -> str:
|
def create_func() -> str:
|
||||||
return "".join(cls.create_completion(model, messages, False, **kwargs))
|
return "".join(cls.create_completion(model, messages, False, **kwargs))
|
||||||
@ -101,8 +102,6 @@ class AsyncProvider(AbstractProvider):
|
|||||||
model: str,
|
model: str,
|
||||||
messages: Messages,
|
messages: Messages,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
*,
|
|
||||||
loop: AbstractEventLoop = None,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> CreateResult:
|
) -> CreateResult:
|
||||||
"""
|
"""
|
||||||
@ -119,9 +118,15 @@ class AsyncProvider(AbstractProvider):
|
|||||||
Returns:
|
Returns:
|
||||||
CreateResult: The result of the completion creation.
|
CreateResult: The result of the completion creation.
|
||||||
"""
|
"""
|
||||||
loop = loop or get_event_loop()
|
try:
|
||||||
coro = cls.create_async(model, messages, **kwargs)
|
loop = asyncio.get_running_loop()
|
||||||
yield loop.run_until_complete(coro)
|
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
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -159,8 +164,6 @@ class AsyncGeneratorProvider(AsyncProvider):
|
|||||||
model: str,
|
model: str,
|
||||||
messages: Messages,
|
messages: Messages,
|
||||||
stream: bool = True,
|
stream: bool = True,
|
||||||
*,
|
|
||||||
loop: AbstractEventLoop = None,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> CreateResult:
|
) -> CreateResult:
|
||||||
"""
|
"""
|
||||||
@ -177,7 +180,16 @@ class AsyncGeneratorProvider(AsyncProvider):
|
|||||||
Returns:
|
Returns:
|
||||||
CreateResult: The result of the streaming completion creation.
|
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)
|
generator = cls.create_async_generator(model, messages, stream=stream, **kwargs)
|
||||||
gen = generator.__aiter__()
|
gen = generator.__aiter__()
|
||||||
|
|
||||||
|
@ -24,3 +24,6 @@ class RetryNoProviderError(Exception):
|
|||||||
|
|
||||||
class VersionNotFoundError(Exception):
|
class VersionNotFoundError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class NestAsyncioError(Exception):
|
||||||
|
pass
|
@ -6,6 +6,7 @@ from selenium.webdriver.common.by import By
|
|||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from os import path
|
from os import path
|
||||||
|
from os import access, R_OK
|
||||||
from . import debug
|
from . import debug
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -40,8 +41,9 @@ def get_browser(
|
|||||||
options = ChromeOptions()
|
options = ChromeOptions()
|
||||||
if proxy:
|
if proxy:
|
||||||
options.add_argument(f'--proxy-server={proxy}')
|
options.add_argument(f'--proxy-server={proxy}')
|
||||||
|
# Check for system driver in docker
|
||||||
driver = '/usr/bin/chromedriver'
|
driver = '/usr/bin/chromedriver'
|
||||||
if not path.isfile(driver):
|
if not path.isfile(driver) or not access(driver, R_OK):
|
||||||
driver = None
|
driver = None
|
||||||
return Chrome(
|
return Chrome(
|
||||||
options=options,
|
options=options,
|
||||||
|
Loading…
Reference in New Issue
Block a user