pr-agent 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pr_agent/__init__.py +1 -0
- pr_agent/agent/__init__.py +0 -0
- pr_agent/agent/pr_agent.py +94 -0
- pr_agent/algo/__init__.py +31 -0
- pr_agent/algo/ai_handlers/base_ai_handler.py +28 -0
- pr_agent/algo/ai_handlers/langchain_ai_handler.py +67 -0
- pr_agent/algo/ai_handlers/litellm_ai_handler.py +153 -0
- pr_agent/algo/ai_handlers/openai_ai_handler.py +67 -0
- pr_agent/algo/file_filter.py +36 -0
- pr_agent/algo/git_patch_processing.py +303 -0
- pr_agent/algo/language_handler.py +67 -0
- pr_agent/algo/pr_processing.py +385 -0
- pr_agent/algo/token_handler.py +69 -0
- pr_agent/algo/types.py +23 -0
- pr_agent/algo/utils.py +663 -0
- pr_agent/cli.py +73 -0
- pr_agent/cli_pip.py +30 -0
- pr_agent/config_loader.py +70 -0
- pr_agent/git_providers/__init__.py +30 -0
- pr_agent/git_providers/azuredevops_provider.py +566 -0
- pr_agent/git_providers/bitbucket_provider.py +408 -0
- pr_agent/git_providers/bitbucket_server_provider.py +354 -0
- pr_agent/git_providers/codecommit_client.py +277 -0
- pr_agent/git_providers/codecommit_provider.py +495 -0
- pr_agent/git_providers/gerrit_provider.py +399 -0
- pr_agent/git_providers/git_provider.py +289 -0
- pr_agent/git_providers/github_provider.py +766 -0
- pr_agent/git_providers/gitlab_provider.py +484 -0
- pr_agent/git_providers/local_git_provider.py +180 -0
- pr_agent/git_providers/utils.py +49 -0
- pr_agent/identity_providers/__init__.py +13 -0
- pr_agent/identity_providers/default_identity_provider.py +9 -0
- pr_agent/identity_providers/identity_provider.py +18 -0
- pr_agent/log/__init__.py +65 -0
- pr_agent/secret_providers/__init__.py +19 -0
- pr_agent/secret_providers/google_cloud_storage_secret_provider.py +34 -0
- pr_agent/secret_providers/secret_provider.py +12 -0
- pr_agent/servers/__init__.py +0 -0
- pr_agent/servers/azuredevops_server_webhook.py +139 -0
- pr_agent/servers/bitbucket_app.py +210 -0
- pr_agent/servers/bitbucket_server_webhook.py +80 -0
- pr_agent/servers/gerrit_server.py +77 -0
- pr_agent/servers/github_action_runner.py +143 -0
- pr_agent/servers/github_app.py +354 -0
- pr_agent/servers/github_polling.py +115 -0
- pr_agent/servers/gitlab_webhook.py +140 -0
- pr_agent/servers/help.py +354 -0
- pr_agent/servers/serverless.py +17 -0
- pr_agent/servers/utils.py +86 -0
- pr_agent/settings/.secrets_template.toml +89 -0
- pr_agent/settings/configuration.toml +212 -0
- pr_agent/settings/custom_labels.toml +16 -0
- pr_agent/settings/ignore.toml +11 -0
- pr_agent/settings/language_extensions.toml +438 -0
- pr_agent/settings/pr_add_docs.toml +126 -0
- pr_agent/settings/pr_code_suggestions_prompts.toml +121 -0
- pr_agent/settings/pr_custom_labels.toml +86 -0
- pr_agent/settings/pr_description_prompts.toml +130 -0
- pr_agent/settings/pr_information_from_user_prompts.toml +53 -0
- pr_agent/settings/pr_line_questions_prompts.toml +53 -0
- pr_agent/settings/pr_questions_prompts.toml +42 -0
- pr_agent/settings/pr_reviewer_prompts.toml +174 -0
- pr_agent/settings/pr_sort_code_suggestions_prompts.toml +46 -0
- pr_agent/settings/pr_update_changelog_prompts.toml +62 -0
- pr_agent/tools/__init__.py +0 -0
- pr_agent/tools/pr_add_docs.py +184 -0
- pr_agent/tools/pr_code_suggestions.py +464 -0
- pr_agent/tools/pr_config.py +47 -0
- pr_agent/tools/pr_description.py +507 -0
- pr_agent/tools/pr_generate_labels.py +178 -0
- pr_agent/tools/pr_help_message.py +99 -0
- pr_agent/tools/pr_information_from_user.py +79 -0
- pr_agent/tools/pr_line_questions.py +107 -0
- pr_agent/tools/pr_questions.py +96 -0
- pr_agent/tools/pr_reviewer.py +420 -0
- pr_agent/tools/pr_similar_issue.py +486 -0
- pr_agent/tools/pr_update_changelog.py +174 -0
- pr_agent-0.2.0.dist-info/LICENSE +202 -0
- pr_agent-0.2.0.dist-info/METADATA +549 -0
- pr_agent-0.2.0.dist-info/RECORD +83 -0
- pr_agent-0.2.0.dist-info/WHEEL +5 -0
- pr_agent-0.2.0.dist-info/entry_points.txt +2 -0
- pr_agent-0.2.0.dist-info/top_level.txt +1 -0
pr_agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
5
|
+
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
|
6
|
+
|
|
7
|
+
from pr_agent.algo.utils import update_settings_from_args
|
|
8
|
+
from pr_agent.config_loader import get_settings
|
|
9
|
+
from pr_agent.git_providers.utils import apply_repo_settings
|
|
10
|
+
from pr_agent.log import get_logger
|
|
11
|
+
from pr_agent.tools.pr_add_docs import PRAddDocs
|
|
12
|
+
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
|
13
|
+
from pr_agent.tools.pr_config import PRConfig
|
|
14
|
+
from pr_agent.tools.pr_description import PRDescription
|
|
15
|
+
from pr_agent.tools.pr_generate_labels import PRGenerateLabels
|
|
16
|
+
from pr_agent.tools.pr_help_message import PRHelpMessage
|
|
17
|
+
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
|
18
|
+
from pr_agent.tools.pr_line_questions import PR_LineQuestions
|
|
19
|
+
from pr_agent.tools.pr_questions import PRQuestions
|
|
20
|
+
from pr_agent.tools.pr_reviewer import PRReviewer
|
|
21
|
+
from pr_agent.tools.pr_similar_issue import PRSimilarIssue
|
|
22
|
+
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
|
23
|
+
|
|
24
|
+
command2class = {
|
|
25
|
+
"auto_review": PRReviewer,
|
|
26
|
+
"answer": PRReviewer,
|
|
27
|
+
"review": PRReviewer,
|
|
28
|
+
"review_pr": PRReviewer,
|
|
29
|
+
"reflect": PRInformationFromUser,
|
|
30
|
+
"reflect_and_review": PRInformationFromUser,
|
|
31
|
+
"describe": PRDescription,
|
|
32
|
+
"describe_pr": PRDescription,
|
|
33
|
+
"improve": PRCodeSuggestions,
|
|
34
|
+
"improve_code": PRCodeSuggestions,
|
|
35
|
+
"ask": PRQuestions,
|
|
36
|
+
"ask_question": PRQuestions,
|
|
37
|
+
"ask_line": PR_LineQuestions,
|
|
38
|
+
"update_changelog": PRUpdateChangelog,
|
|
39
|
+
"config": PRConfig,
|
|
40
|
+
"settings": PRConfig,
|
|
41
|
+
"help": PRHelpMessage,
|
|
42
|
+
"similar_issue": PRSimilarIssue,
|
|
43
|
+
"add_docs": PRAddDocs,
|
|
44
|
+
"generate_labels": PRGenerateLabels,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
commands = list(command2class.keys())
|
|
48
|
+
|
|
49
|
+
class PRAgent:
|
|
50
|
+
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
|
51
|
+
self.ai_handler = ai_handler # will be initialized in run_action
|
|
52
|
+
self.forbidden_cli_args = ['enable_auto_approval']
|
|
53
|
+
|
|
54
|
+
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
|
55
|
+
# First, apply repo specific settings if exists
|
|
56
|
+
apply_repo_settings(pr_url)
|
|
57
|
+
|
|
58
|
+
# Then, apply user specific settings if exists
|
|
59
|
+
if isinstance(request, str):
|
|
60
|
+
request = request.replace("'", "\\'")
|
|
61
|
+
lexer = shlex.shlex(request, posix=True)
|
|
62
|
+
lexer.whitespace_split = True
|
|
63
|
+
action, *args = list(lexer)
|
|
64
|
+
else:
|
|
65
|
+
action, *args = request
|
|
66
|
+
|
|
67
|
+
if args:
|
|
68
|
+
for forbidden_arg in self.forbidden_cli_args:
|
|
69
|
+
for arg in args:
|
|
70
|
+
if forbidden_arg in arg:
|
|
71
|
+
get_logger().error(f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file.")
|
|
72
|
+
return False
|
|
73
|
+
args = update_settings_from_args(args)
|
|
74
|
+
|
|
75
|
+
action = action.lstrip("/").lower()
|
|
76
|
+
with get_logger().contextualize(command=action):
|
|
77
|
+
get_logger().info("PR-Agent request handler started", analytics=True)
|
|
78
|
+
if action == "reflect_and_review":
|
|
79
|
+
get_settings().pr_reviewer.ask_and_reflect = True
|
|
80
|
+
if action == "answer":
|
|
81
|
+
if notify:
|
|
82
|
+
notify()
|
|
83
|
+
await PRReviewer(pr_url, is_answer=True, args=args, ai_handler=self.ai_handler).run()
|
|
84
|
+
elif action == "auto_review":
|
|
85
|
+
await PRReviewer(pr_url, is_auto=True, args=args, ai_handler=self.ai_handler).run()
|
|
86
|
+
elif action in command2class:
|
|
87
|
+
if notify:
|
|
88
|
+
notify()
|
|
89
|
+
|
|
90
|
+
await command2class[action](pr_url, ai_handler=self.ai_handler, args=args).run()
|
|
91
|
+
else:
|
|
92
|
+
return False
|
|
93
|
+
return True
|
|
94
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MAX_TOKENS = {
|
|
2
|
+
'text-embedding-ada-002': 8000,
|
|
3
|
+
'gpt-3.5-turbo': 4000,
|
|
4
|
+
'gpt-3.5-turbo-0613': 4000,
|
|
5
|
+
'gpt-3.5-turbo-0301': 4000,
|
|
6
|
+
'gpt-3.5-turbo-16k': 16000,
|
|
7
|
+
'gpt-3.5-turbo-16k-0613': 16000,
|
|
8
|
+
'gpt-4': 8000,
|
|
9
|
+
'gpt-4-0613': 8000,
|
|
10
|
+
'gpt-4-32k': 32000,
|
|
11
|
+
'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens
|
|
12
|
+
'gpt-4-0125-preview': 128000, # 128K, but may be limited by config.max_model_tokens
|
|
13
|
+
'claude-instant-1': 100000,
|
|
14
|
+
'claude-2': 100000,
|
|
15
|
+
'command-nightly': 4096,
|
|
16
|
+
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
|
17
|
+
'meta-llama/Llama-2-7b-chat-hf': 4096,
|
|
18
|
+
'vertex_ai/codechat-bison': 6144,
|
|
19
|
+
'vertex_ai/codechat-bison-32k': 32000,
|
|
20
|
+
'codechat-bison': 6144,
|
|
21
|
+
'codechat-bison-32k': 32000,
|
|
22
|
+
'anthropic.claude-instant-v1': 100000,
|
|
23
|
+
'anthropic.claude-v1': 100000,
|
|
24
|
+
'anthropic.claude-v2': 100000,
|
|
25
|
+
'anthropic/claude-3-opus-20240229': 100000,
|
|
26
|
+
'bedrock/anthropic.claude-instant-v1': 100000,
|
|
27
|
+
'bedrock/anthropic.claude-v2': 100000,
|
|
28
|
+
'bedrock/anthropic.claude-v2:1': 100000,
|
|
29
|
+
'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000,
|
|
30
|
+
'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000,
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class BaseAiHandler(ABC):
|
|
4
|
+
"""
|
|
5
|
+
This class defines the interface for an AI handler to be used by the PR Agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def __init__(self):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def deployment_id(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
|
19
|
+
"""
|
|
20
|
+
This method should be implemented to return a chat completion from the AI model.
|
|
21
|
+
Args:
|
|
22
|
+
model (str): the name of the model to use for the chat completion
|
|
23
|
+
system (str): the system message string to use for the chat completion
|
|
24
|
+
user (str): the user message string to use for the chat completion
|
|
25
|
+
temperature (float): the temperature to use for the chat completion
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from langchain.chat_models import ChatOpenAI, AzureChatOpenAI
|
|
3
|
+
from langchain.schema import SystemMessage, HumanMessage
|
|
4
|
+
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
8
|
+
from pr_agent.config_loader import get_settings
|
|
9
|
+
from pr_agent.log import get_logger
|
|
10
|
+
|
|
11
|
+
from openai.error import APIError, RateLimitError, Timeout, TryAgain
|
|
12
|
+
from retry import retry
|
|
13
|
+
import functools
|
|
14
|
+
|
|
15
|
+
OPENAI_RETRIES = 5
|
|
16
|
+
|
|
17
|
+
class LangChainOpenAIHandler(BaseAiHandler):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
# Initialize OpenAIHandler specific attributes here
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.azure = get_settings().get("OPENAI.API_TYPE", "").lower() == "azure"
|
|
22
|
+
try:
|
|
23
|
+
if self.azure:
|
|
24
|
+
# using a partial function so we can set the deployment_id later to support fallback_deployments
|
|
25
|
+
# but still need to access the other settings now so we can raise a proper exception if they're missing
|
|
26
|
+
self._chat = functools.partial(
|
|
27
|
+
lambda **kwargs: AzureChatOpenAI(**kwargs),
|
|
28
|
+
openai_api_key=get_settings().openai.key,
|
|
29
|
+
openai_api_base=get_settings().openai.api_base,
|
|
30
|
+
openai_api_version=get_settings().openai.api_version,
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
self._chat = ChatOpenAI(openai_api_key=get_settings().openai.key)
|
|
34
|
+
except AttributeError as e:
|
|
35
|
+
if getattr(e, "name"):
|
|
36
|
+
raise ValueError(f"OpenAI {e.name} is required") from e
|
|
37
|
+
else:
|
|
38
|
+
raise e
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def chat(self):
|
|
42
|
+
if self.azure:
|
|
43
|
+
# we must set the deployment_id only here (instead of the __init__ method) to support fallback_deployments
|
|
44
|
+
return self._chat(deployment_name=self.deployment_id)
|
|
45
|
+
else:
|
|
46
|
+
return self._chat
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def deployment_id(self):
|
|
50
|
+
"""
|
|
51
|
+
Returns the deployment ID for the OpenAI API.
|
|
52
|
+
"""
|
|
53
|
+
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
|
|
54
|
+
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
|
|
55
|
+
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
|
|
56
|
+
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
|
57
|
+
try:
|
|
58
|
+
messages=[SystemMessage(content=system), HumanMessage(content=user)]
|
|
59
|
+
|
|
60
|
+
# get a chat completion from the formatted messages
|
|
61
|
+
resp = self.chat(messages, model=model, temperature=temperature)
|
|
62
|
+
finish_reason="completed"
|
|
63
|
+
return resp.content, finish_reason
|
|
64
|
+
|
|
65
|
+
except (Exception) as e:
|
|
66
|
+
get_logger().error("Unknown error during OpenAI inference: ", e)
|
|
67
|
+
raise e
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
import litellm
|
|
5
|
+
import openai
|
|
6
|
+
from litellm import acompletion
|
|
7
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
|
8
|
+
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
9
|
+
from pr_agent.config_loader import get_settings
|
|
10
|
+
from pr_agent.log import get_logger
|
|
11
|
+
|
|
12
|
+
OPENAI_RETRIES = 5
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LiteLLMAIHandler(BaseAiHandler):
|
|
16
|
+
"""
|
|
17
|
+
This class handles interactions with the OpenAI API for chat completions.
|
|
18
|
+
It initializes the API key and other settings from a configuration file,
|
|
19
|
+
and provides a method for performing chat completions using the OpenAI ChatCompletion API.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""
|
|
24
|
+
Initializes the OpenAI API key and other settings from a configuration file.
|
|
25
|
+
Raises a ValueError if the OpenAI key is missing.
|
|
26
|
+
"""
|
|
27
|
+
self.azure = False
|
|
28
|
+
self.aws_bedrock_client = None
|
|
29
|
+
self.api_base = None
|
|
30
|
+
self.repetition_penalty = None
|
|
31
|
+
if get_settings().get("OPENAI.KEY", None):
|
|
32
|
+
openai.api_key = get_settings().openai.key
|
|
33
|
+
litellm.openai_key = get_settings().openai.key
|
|
34
|
+
if get_settings().get("litellm.use_client"):
|
|
35
|
+
litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
|
|
36
|
+
assert litellm_token, "LITELLM_TOKEN is required"
|
|
37
|
+
os.environ["LITELLM_TOKEN"] = litellm_token
|
|
38
|
+
litellm.use_client = True
|
|
39
|
+
if get_settings().get("LITELLM.DROP_PARAMS", None):
|
|
40
|
+
litellm.drop_params = get_settings().litellm.drop_params
|
|
41
|
+
if get_settings().get("OPENAI.ORG", None):
|
|
42
|
+
litellm.organization = get_settings().openai.org
|
|
43
|
+
if get_settings().get("OPENAI.API_TYPE", None):
|
|
44
|
+
if get_settings().openai.api_type == "azure":
|
|
45
|
+
self.azure = True
|
|
46
|
+
litellm.azure_key = get_settings().openai.key
|
|
47
|
+
if get_settings().get("OPENAI.API_VERSION", None):
|
|
48
|
+
litellm.api_version = get_settings().openai.api_version
|
|
49
|
+
if get_settings().get("OPENAI.API_BASE", None):
|
|
50
|
+
litellm.api_base = get_settings().openai.api_base
|
|
51
|
+
if get_settings().get("ANTHROPIC.KEY", None):
|
|
52
|
+
litellm.anthropic_key = get_settings().anthropic.key
|
|
53
|
+
if get_settings().get("COHERE.KEY", None):
|
|
54
|
+
litellm.cohere_key = get_settings().cohere.key
|
|
55
|
+
if get_settings().get("REPLICATE.KEY", None):
|
|
56
|
+
litellm.replicate_key = get_settings().replicate.key
|
|
57
|
+
if get_settings().get("REPLICATE.KEY", None):
|
|
58
|
+
litellm.replicate_key = get_settings().replicate.key
|
|
59
|
+
if get_settings().get("HUGGINGFACE.KEY", None):
|
|
60
|
+
litellm.huggingface_key = get_settings().huggingface.key
|
|
61
|
+
if get_settings().get("HUGGINGFACE.API_BASE", None) and 'huggingface' in get_settings().config.model:
|
|
62
|
+
litellm.api_base = get_settings().huggingface.api_base
|
|
63
|
+
self.api_base = get_settings().huggingface.api_base
|
|
64
|
+
if get_settings().get("HUGGINGFACE.REPITITION_PENALTY", None):
|
|
65
|
+
self.repetition_penalty = float(get_settings().huggingface.repetition_penalty)
|
|
66
|
+
if get_settings().get("VERTEXAI.VERTEX_PROJECT", None):
|
|
67
|
+
litellm.vertex_project = get_settings().vertexai.vertex_project
|
|
68
|
+
litellm.vertex_location = get_settings().get(
|
|
69
|
+
"VERTEXAI.VERTEX_LOCATION", None
|
|
70
|
+
)
|
|
71
|
+
if get_settings().get("AWS.BEDROCK_REGION", None):
|
|
72
|
+
litellm.AmazonAnthropicConfig.max_tokens_to_sample = 2000
|
|
73
|
+
litellm.AmazonAnthropicClaude3Config.max_tokens = 2000
|
|
74
|
+
self.aws_bedrock_client = boto3.client(
|
|
75
|
+
service_name="bedrock-runtime",
|
|
76
|
+
region_name=get_settings().aws.bedrock_region,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def prepare_logs(self, response, system, user, resp, finish_reason):
|
|
80
|
+
response_log = response.dict().copy()
|
|
81
|
+
response_log['system'] = system
|
|
82
|
+
response_log['user'] = user
|
|
83
|
+
response_log['output'] = resp
|
|
84
|
+
response_log['finish_reason'] = finish_reason
|
|
85
|
+
if hasattr(self, 'main_pr_language'):
|
|
86
|
+
response_log['main_pr_language'] = self.main_pr_language
|
|
87
|
+
else:
|
|
88
|
+
response_log['main_pr_language'] = 'unknown'
|
|
89
|
+
return response_log
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def deployment_id(self):
|
|
93
|
+
"""
|
|
94
|
+
Returns the deployment ID for the OpenAI API.
|
|
95
|
+
"""
|
|
96
|
+
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
|
|
97
|
+
|
|
98
|
+
@retry(
|
|
99
|
+
retry=retry_if_exception_type((openai.APIError, openai.APIConnectionError, openai.Timeout)), # No retry on RateLimitError
|
|
100
|
+
stop=stop_after_attempt(OPENAI_RETRIES)
|
|
101
|
+
)
|
|
102
|
+
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
|
103
|
+
try:
|
|
104
|
+
resp, finish_reason = None, None
|
|
105
|
+
deployment_id = self.deployment_id
|
|
106
|
+
if self.azure:
|
|
107
|
+
model = 'azure/' + model
|
|
108
|
+
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
|
109
|
+
kwargs = {
|
|
110
|
+
"model": model,
|
|
111
|
+
"deployment_id": deployment_id,
|
|
112
|
+
"messages": messages,
|
|
113
|
+
"temperature": temperature,
|
|
114
|
+
"force_timeout": get_settings().config.ai_timeout,
|
|
115
|
+
"api_base" : self.api_base,
|
|
116
|
+
}
|
|
117
|
+
if self.aws_bedrock_client:
|
|
118
|
+
kwargs["aws_bedrock_client"] = self.aws_bedrock_client
|
|
119
|
+
if self.repetition_penalty:
|
|
120
|
+
kwargs["repetition_penalty"] = self.repetition_penalty
|
|
121
|
+
|
|
122
|
+
get_logger().debug("Prompts", artifact={"system": system, "user": user})
|
|
123
|
+
|
|
124
|
+
if get_settings().config.verbosity_level >= 2:
|
|
125
|
+
get_logger().info(f"\nSystem prompt:\n{system}")
|
|
126
|
+
get_logger().info(f"\nUser prompt:\n{user}")
|
|
127
|
+
|
|
128
|
+
response = await acompletion(**kwargs)
|
|
129
|
+
except (openai.APIError, openai.Timeout) as e:
|
|
130
|
+
get_logger().error("Error during OpenAI inference: ", e)
|
|
131
|
+
raise
|
|
132
|
+
except (openai.RateLimitError) as e:
|
|
133
|
+
get_logger().error("Rate limit error during OpenAI inference: ", e)
|
|
134
|
+
raise
|
|
135
|
+
except (Exception) as e:
|
|
136
|
+
get_logger().error("Unknown error during OpenAI inference: ", e)
|
|
137
|
+
raise openai.APIError from e
|
|
138
|
+
if response is None or len(response["choices"]) == 0:
|
|
139
|
+
raise openai.APIError
|
|
140
|
+
else:
|
|
141
|
+
resp = response["choices"][0]['message']['content']
|
|
142
|
+
finish_reason = response["choices"][0]["finish_reason"]
|
|
143
|
+
get_logger().debug(f"\nAI response:\n{resp}")
|
|
144
|
+
|
|
145
|
+
# log the full response for debugging
|
|
146
|
+
response_log = self.prepare_logs(response, system, user, resp, finish_reason)
|
|
147
|
+
get_logger().debug("Full_response", artifact=response_log)
|
|
148
|
+
|
|
149
|
+
# for CLI debugging
|
|
150
|
+
if get_settings().config.verbosity_level >= 2:
|
|
151
|
+
get_logger().info(f"\nAI response:\n{resp}")
|
|
152
|
+
|
|
153
|
+
return resp, finish_reason
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
2
|
+
import openai
|
|
3
|
+
from openai.error import APIError, RateLimitError, Timeout, TryAgain
|
|
4
|
+
from retry import retry
|
|
5
|
+
|
|
6
|
+
from pr_agent.config_loader import get_settings
|
|
7
|
+
from pr_agent.log import get_logger
|
|
8
|
+
|
|
9
|
+
OPENAI_RETRIES = 5
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIHandler(BaseAiHandler):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
# Initialize OpenAIHandler specific attributes here
|
|
15
|
+
try:
|
|
16
|
+
super().__init__()
|
|
17
|
+
openai.api_key = get_settings().openai.key
|
|
18
|
+
if get_settings().get("OPENAI.ORG", None):
|
|
19
|
+
openai.organization = get_settings().openai.org
|
|
20
|
+
if get_settings().get("OPENAI.API_TYPE", None):
|
|
21
|
+
if get_settings().openai.api_type == "azure":
|
|
22
|
+
self.azure = True
|
|
23
|
+
openai.azure_key = get_settings().openai.key
|
|
24
|
+
if get_settings().get("OPENAI.API_VERSION", None):
|
|
25
|
+
openai.api_version = get_settings().openai.api_version
|
|
26
|
+
if get_settings().get("OPENAI.API_BASE", None):
|
|
27
|
+
openai.api_base = get_settings().openai.api_base
|
|
28
|
+
|
|
29
|
+
except AttributeError as e:
|
|
30
|
+
raise ValueError("OpenAI key is required") from e
|
|
31
|
+
@property
|
|
32
|
+
def deployment_id(self):
|
|
33
|
+
"""
|
|
34
|
+
Returns the deployment ID for the OpenAI API.
|
|
35
|
+
"""
|
|
36
|
+
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
|
|
37
|
+
|
|
38
|
+
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
|
|
39
|
+
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
|
|
40
|
+
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
|
41
|
+
try:
|
|
42
|
+
deployment_id = self.deployment_id
|
|
43
|
+
get_logger().info("System: ", system)
|
|
44
|
+
get_logger().info("User: ", user)
|
|
45
|
+
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
|
46
|
+
|
|
47
|
+
chat_completion = await openai.ChatCompletion.acreate(
|
|
48
|
+
model=model,
|
|
49
|
+
deployment_id=deployment_id,
|
|
50
|
+
messages=messages,
|
|
51
|
+
temperature=temperature,
|
|
52
|
+
)
|
|
53
|
+
resp = chat_completion["choices"][0]['message']['content']
|
|
54
|
+
finish_reason = chat_completion["choices"][0]["finish_reason"]
|
|
55
|
+
usage = chat_completion.get("usage")
|
|
56
|
+
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
|
|
57
|
+
model=model, usage=usage)
|
|
58
|
+
return resp, finish_reason
|
|
59
|
+
except (APIError, Timeout, TryAgain) as e:
|
|
60
|
+
get_logger().error("Error during OpenAI inference: ", e)
|
|
61
|
+
raise
|
|
62
|
+
except (RateLimitError) as e:
|
|
63
|
+
get_logger().error("Rate limit error during OpenAI inference: ", e)
|
|
64
|
+
raise
|
|
65
|
+
except (Exception) as e:
|
|
66
|
+
get_logger().error("Unknown error during OpenAI inference: ", e)
|
|
67
|
+
raise TryAgain from e
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from pr_agent.config_loader import get_settings
|
|
5
|
+
|
|
6
|
+
def filter_ignored(files):
|
|
7
|
+
"""
|
|
8
|
+
Filter out files that match the ignore patterns.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
# load regex patterns, and translate glob patterns to regex
|
|
13
|
+
patterns = get_settings().ignore.regex
|
|
14
|
+
if isinstance(patterns, str):
|
|
15
|
+
patterns = [patterns]
|
|
16
|
+
glob_setting = get_settings().ignore.glob
|
|
17
|
+
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
|
|
18
|
+
glob_setting = glob_setting.strip('[]').split(",")
|
|
19
|
+
patterns += [fnmatch.translate(glob) for glob in glob_setting]
|
|
20
|
+
|
|
21
|
+
# compile all valid patterns
|
|
22
|
+
compiled_patterns = []
|
|
23
|
+
for r in patterns:
|
|
24
|
+
try:
|
|
25
|
+
compiled_patterns.append(re.compile(r))
|
|
26
|
+
except re.error:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# keep filenames that _don't_ match the ignore regex
|
|
30
|
+
for r in compiled_patterns:
|
|
31
|
+
files = [f for f in files if (f.filename and not r.match(f.filename))]
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Could not filter file list: {e}")
|
|
35
|
+
|
|
36
|
+
return files
|