4
from typing import Optional, Tuple
6
if sys.version_info < (3, 8):
7
from typing_extensions import Literal
9
from typing import Literal
11
from github import Github
12
from openai import OpenAI
13
from tenacity import retry, stop_after_attempt, wait_random_exponential
15
GITHUB_TOKEN = os.environ.get("GITHUB_API_TOKEN")
16
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
18
client = OpenAI(api_key=OPENAI_API_KEY)
20
Model = Literal["gpt-4", "gpt-3.5-turbo", "vicuna-7b-v1.1"]
22
CC_TYPES = os.environ.get(
42
CC_SCOPES = os.environ.get(
59
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
60
def chat_completion_with_backoff(**kwargs):
61
"""Call OpenAI's chat completion API with exponential backoff."""
62
return client.chat.completions.create(**kwargs)
65
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
68
repo_name: str = "wandb/wandb",
69
get_diff: bool = True,
70
) -> Tuple[str, Optional[str]]:
71
"""Get the title and diff of a PR."""
72
g = Github(GITHUB_TOKEN)
73
repo = g.get_repo(repo_name)
75
# Get specific pull request by number
76
pr = repo.get_pull(pr_number)
82
files = pr.get_files()
86
file.patch or file.filename # use filename if patch is empty
88
if "_pb2" not in file.filename # ignore autogenerated protobuf files
98
model: Model = "gpt-4",
99
repo_name: str = "wandb/wandb",
101
"""Check whether a PR title follows the conventional commit format."""
102
pr_title, _ = get_pr_info(pr_number, repo_name=repo_name, get_diff=False)
108
"Your task is to check whether the title for a GitHub pull request "
109
"follows the conventional commit format: "
110
f"<type>(<scope>): <description>. The possible types are: {CC_TYPES}."
111
f"The possible scopes are: {CC_SCOPES}."
112
"The description must start with a verb in the imperative mood and be lower case."
113
"The user will provide the current PR title. You must respond with 'yes' or 'no'."
118
"content": f"{pr_title}",
122
response = chat_completion_with_backoff(
127
is_compliant = response.choices[0]["message"]["content"].lower().strip()
130
return is_compliant == "yes"
133
def generate_pr_title(
135
model: Model = "gpt-4",
136
repo_name: str = "wandb/wandb",
138
"""Generate a PR title for a given PR number using the given model."""
143
"Your task is to write a title for a GitHub pull request "
144
"that will follow the conventional commit format and capture the essence of the change: "
145
f"<type>(<scope>): <description>. The possible types are: {CC_TYPES}."
146
f"The possible scopes are: {CC_SCOPES}."
147
"The description must start with a verb in the imperative mood and be lower case."
148
"For context, the user will provide the current title and the diff of the pull request."
149
"and their corresponding labels."
150
"You must respond in the format: <type>(<scope>): <description>."
151
"Be concise and specific. If you are unsure, keep the original title."
152
"Even if you think the correct type or scope is missing, you must only use the provided options."
158
"content": "Title: {{TITLE}}. Diff: {{DIFF}}.",
162
pr_title, diff = get_pr_info(pr_number, repo_name=repo_name)
164
# todo: check context limit, strip diff if too long
166
messages[-1]["content"] = messages[-1]["content"].replace("{{TITLE}}", pr_title)
167
messages[-1]["content"] = messages[-1]["content"].replace("{{DIFF}}", diff)
169
completion = chat_completion_with_backoff(
173
suggested_title = completion.choices[0]["message"]["content"]
175
return suggested_title
178
if __name__ == "__main__":
179
parser = argparse.ArgumentParser()
180
# add two subparsers: one for the "generate" command and one for the "check" command
181
subparsers = parser.add_subparsers(dest="command")
183
generate_parser = subparsers.add_parser("generate", help="Generate PR title")
184
generate_parser.add_argument("pr_number", type=int, help="Pull Request number")
186
check_parser = subparsers.add_parser("check", help="Check PR title")
187
check_parser.add_argument("pr_number", type=int, help="Pull Request number")
189
args = parser.parse_args()
191
if args.command == "generate":
192
title = generate_pr_title(args.pr_number)
194
elif args.command == "check":
195
is_conventional_commit_compliant = check_pr_title(args.pr_number)
196
if not is_conventional_commit_compliant:
198
"PR title is not compliant with the conventional commit recommendations. \n"
199
"Comment on your PR with `/suggest-title` to get a suggestion or "
200
"`/fix-title` to ask the pr-title-bot to fix it for you."
203
print("Invalid command. Use 'generate' or 'check'")