Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hl/ask line #661

Merged
merged 9 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/ASK.md

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes walkthrough

                         Relevant files
Enhancement
pr_agent.py (+2/-0)
Integrate Line-Specific Question
Handling in PR Agent
git_patch_processing.py (+54/-0)
Add Function to Extract Specific Lines
from Patches
git_provider.py (+3/-0)
Define Method for Replying to Comments
by ID in Git Provider Interface
github_provider.py (+17/-0)
Implement Comment Reply and Reaction
Features for GitHub Provider
github_app.py (+20/-0)
Add Handling for Line Comments in GitHub
App
pr_line_questions.py (+105/-0)
Implement Line-Specific Question
Handling
Configuration changes
config_loader.py (+1/-0)
Update Configuration Loader to Include
Line Questions Prompts
pr_line_questions_prompts.toml (+53/-0)
Add Prompts Configuration for
Line-Specific Questions
Documentation
ASK.md (+9/-0)
Document Line-Specific Questions Feature

Copy link
Collaborator Author

@hussam789 hussam789 Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relevant files
Enhancement

pr_agent.py (+2/-0)
Integrate Line-Specific Question Handling in PR Agent

pr_agent/agent/pr_agent.py

  • Import PR_LineQuestions for handling line-specific questions.
  • Add ask_line to the command mapping, allowing for line-specific
    queries.
  • git_patch_processing.py (+54/-0)
    Add Function to Extract Specific Lines from Patches

    pr_agent/algo/git_patch_processing.py

  • Implement extract_hunk_lines_from_patch to extract specific lines from
    a patch.
  • git_provider.py (+3/-0)
    Define Method for Replying to Comments by ID in Git
    Provider Interface

    pr_agent/git_providers/git_provider.py

    • Define reply_to_comment_from_comment_id method stub.
    github_provider.py (+17/-0)
    Implement Comment Reply and Reaction Features for GitHub
    Provider

    pr_agent/git_providers/github_provider.py

  • Implement reply_to_comment_from_comment_id for GitHub.
  • Add logic to add "eyes" reaction to comments.
  • github_app.py (+20/-0)
    Add Handling for Line Comments in GitHub App

    pr_agent/servers/github_app.py

    • Handle line comments in GitHub App server logic.
    pr_line_questions.py (+105/-0)
    Implement Line-Specific Question Handling

    pr_agent/tools/pr_line_questions.py

  • Implement PR_LineQuestions class for answering line-specific
    questions.
  • Configuration changes

    config_loader.py (+1/-0)
    Update Configuration Loader to Include Line Questions
    Prompts

    pr_agent/config_loader.py

  • Include pr_line_questions_prompts.toml in the configuration loader.
  • pr_line_questions_prompts.toml (+53/-0)
    Add Prompts Configuration for Line-Specific Questions

    pr_agent/settings/pr_line_questions_prompts.toml

    • Add prompts configuration for line-specific questions.
    Documentation

    ASK.md (+9/-0)
    Document Line-Specific Questions Feature

    docs/ASK.md

    • Document how to use the /ask command on specific lines of code.

    Copy link
    Collaborator Author

    @hussam789 hussam789 Feb 18, 2024

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Enhancement
    pr_agent.py (+2/-0)
    Integrate Line-Specific Question Handling in PR Agent       

    pr_agent/agent/pr_agent.py

  • Import PR_LineQuestions for handling line-specific questions.
  • Add ask_line to the command mapping, allowing for line-specific
    queries.
  • git_patch_processing.py (+54/-0)
    Add Function to Extract Specific Lines from Patches           

    pr_agent/algo/git_patch_processing.py

  • Implement extract_hunk_lines_from_patch to extract specific lines from
    a patch.
  • git_provider.py (+3/-0)
    Define Method for Replying to Comments by ID in Git Provider Interface

    pr_agent/git_providers/git_provider.py

    • Define reply_to_comment_from_comment_id method stub.
    github_provider.py (+17/-0)
    Implement Comment Reply and Reaction Features for GitHub Provider

    pr_agent/git_providers/github_provider.py

  • Implement reply_to_comment_from_comment_id for GitHub.
  • Add logic to add "eyes" reaction to comments.
  • github_app.py (+20/-0)
    Add Handling for Line Comments in GitHub App                         

    pr_agent/servers/github_app.py

    • Handle line comments in GitHub App server logic.
    pr_line_questions.py (+105/-0)
    Implement Line-Specific Question Handling                               

    pr_agent/tools/pr_line_questions.py

  • Implement PR_LineQuestions class for answering line-specific
    questions.
  • Configuration changes
    config_loader.py (+1/-0)
    Update Configuration Loader to Include Line Questions Prompts

    pr_agent/config_loader.py

  • Include pr_line_questions_prompts.toml in the configuration loader.
  • pr_line_questions_prompts.toml (+53/-0)
    Add Prompts Configuration for Line-Specific Questions       

    pr_agent/settings/pr_line_questions_prompts.toml

    • Add prompts configuration for line-specific questions.
    Documentation
    ASK.md (+9/-0)
    Document Line-Specific Questions Feature                                 

    docs/ASK.md

    • Document how to use the /ask command on specific lines of code.

    Original file line number Diff line number Diff line change
    Expand Up @@ -12,4 +12,13 @@ ___
    <kbd><img src=https://codium.ai/images/pr_agent/ask.png width="768"></kbd>
    ___

    ## Ask lines
    mrT23 marked this conversation as resolved.
    Show resolved Hide resolved
    You can run `/ask` on specific lines of code in the PR from the PR's diff view. The tool will answer questions based on the code changes in the selected lines.
    - Click on the '+' sign next to the line number to select the line.
    - To select multiple lines, click on the '+' sign of the first line and then hold and drag to select the rest of the lines.
    - write `/ask "..."` in the comment box and press `Add single comment` button.

    <kbd><img src=https://codium.ai/images/pr_agent/Ask_line.png width="768"></kbd>


    Note that the tool does not have "memory" of previous questions, and answers each question independently.
    2 changes: 2 additions & 0 deletions pr_agent/agent/pr_agent.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -14,6 +14,7 @@
    from pr_agent.tools.pr_description import PRDescription
    from pr_agent.tools.pr_generate_labels import PRGenerateLabels
    from pr_agent.tools.pr_information_from_user import PRInformationFromUser
    from pr_agent.tools.pr_line_questions import PR_LineQuestions
    from pr_agent.tools.pr_questions import PRQuestions
    from pr_agent.tools.pr_reviewer import PRReviewer
    from pr_agent.tools.pr_similar_issue import PRSimilarIssue
    Expand All @@ -32,6 +33,7 @@
    "improve_code": PRCodeSuggestions,
    "ask": PRQuestions,
    "ask_question": PRQuestions,
    "ask_line": PR_LineQuestions,
    "update_changelog": PRUpdateChangelog,
    "config": PRConfig,
    "settings": PRConfig,
    Expand Down
    56 changes: 56 additions & 0 deletions pr_agent/algo/git_patch_processing.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -245,3 +245,59 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
    patch_with_lines_str += f"{line_old}\n"

    return patch_with_lines_str.rstrip()


    def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, side) -> tuple[str, str]:
    Copy link
    Collaborator

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    תהיה בנאדם, תוסיף דוקסטרינג קצר. אח״כ רוצים להשתמש בזה שוב, וצריכים לנחש מה הפונקציה עושה


    patch_with_lines_str = f"\n\n## file: '{file_name.strip()}'\n\n"
    selected_lines = ""
    patch_lines = patch.splitlines()
    RE_HUNK_HEADER = re.compile(
    r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
    match = None
    start1, size1, start2, size2 = -1, -1, -1, -1
    skip_hunk = False
    selected_lines_num = 0
    for line in patch_lines:
    if 'no newline at end of file' in line.lower():
    continue

    if line.startswith('@@'):
    skip_hunk = False
    selected_lines_num = 0
    header_line = line

    match = RE_HUNK_HEADER.match(line)

    res = list(match.groups())
    for i in range(len(res)):
    if res[i] is None:
    res[i] = 0
    try:
    start1, size1, start2, size2 = map(int, res[:4])
    except: # '@@ -0,0 +1 @@' case
    start1, size1, size2 = map(int, res[:3])
    start2 = 0

    # check if line range is in this hunk
    if side.lower() == 'left':
    # check if line range is in this hunk
    if not (start1 <= line_start <= start1 + size1):
    skip_hunk = True
    continue
    elif side.lower() == 'right':
    if not (start2 <= line_start <= start2 + size2):
    skip_hunk = True
    continue
    patch_with_lines_str += f'\n{header_line}\n'

    elif not skip_hunk:
    if side.lower() == 'right' and line_start <= start2 + selected_lines_num <= line_end:
    selected_lines += line + '\n'
    if side.lower() == 'left' and start1 <= selected_lines_num + start1 <= line_end:
    selected_lines += line + '\n'
    patch_with_lines_str += line + '\n'
    if not line.startswith('-'): # currently we don't support /ask line for deleted lines
    selected_lines_num += 1

    return patch_with_lines_str.rstrip(), selected_lines.rstrip()
    1 change: 1 addition & 0 deletions pr_agent/config_loader.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -18,6 +18,7 @@
    "settings/language_extensions.toml",
    "settings/pr_reviewer_prompts.toml",
    "settings/pr_questions_prompts.toml",
    "settings/pr_line_questions_prompts.toml",
    "settings/pr_description_prompts.toml",
    "settings/pr_code_suggestions_prompts.toml",
    "settings/pr_sort_code_suggestions_prompts.toml",
    Expand Down
    2 changes: 1 addition & 1 deletion pr_agent/git_providers/azuredevops_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -444,7 +444,7 @@ def get_issue_comments(self):
    "Azure DevOps provider does not support issue comments yet"
    )

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    return True

    def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
    Expand Down
    2 changes: 1 addition & 1 deletion pr_agent/git_providers/bitbucket_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -298,7 +298,7 @@ def get_issue_comments(self):
    "Bitbucket provider does not support issue comments yet"
    )

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    return True

    def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
    Expand Down
    2 changes: 1 addition & 1 deletion pr_agent/git_providers/bitbucket_server_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -288,7 +288,7 @@ def get_issue_comments(self):
    "Bitbucket provider does not support issue comments yet"
    )

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    return True

    def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
    Expand Down
    2 changes: 1 addition & 1 deletion pr_agent/git_providers/codecommit_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -297,7 +297,7 @@ def get_repo_settings(self):
    settings_filename = ".pr_agent.toml"
    return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True)

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    get_logger().info("CodeCommit provider does not support eyes reaction yet")
    return True

    Expand Down
    2 changes: 1 addition & 1 deletion pr_agent/git_providers/gerrit_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -212,7 +212,7 @@ def get_pr_labels(self):
    raise NotImplementedError(
    'Getting labels is not implemented for the gerrit provider')

    def add_eyes_reaction(self, issue_comment_id: int):
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False):
    raise NotImplementedError(
    'Adding reactions is not implemented for the gerrit provider')

    Expand Down
    5 changes: 4 additions & 1 deletion pr_agent/git_providers/git_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -44,6 +44,9 @@ def get_pr_description_full(self) -> str:
    def edit_comment(self, comment, body: str):
    pass

    def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
    pass

    def get_pr_description(self, *, full: bool = True) -> str:
    from pr_agent.config_loader import get_settings
    from pr_agent.algo.utils import clip_tokens
    Expand Down Expand Up @@ -159,7 +162,7 @@ def get_repo_labels(self):
    pass

    @abstractmethod
    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    pass

    @abstractmethod
    Expand Down
    21 changes: 20 additions & 1 deletion pr_agent/git_providers/github_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -384,6 +384,16 @@ def publish_code_suggestions(self, code_suggestions: list) -> bool:
    def edit_comment(self, comment, body: str):
    comment.edit(body=body)

    def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
    try:
    # self.pr.get_issue_comment(comment_id).edit(body)
    headers, data_patch = self.pr._requester.requestJsonAndCheck(
    "POST", f"https://api.github.com/repos/{self.repo}/pulls/{self.pr_num}/comments/{comment_id}/replies",
    input={"body": body}
    )
    except Exception as e:
    get_logger().exception(f"Failed to reply comment, error: {e}")

    def remove_initial_comment(self):
    try:
    for comment in getattr(self.pr, 'comments_list', []):
    Expand Down Expand Up @@ -442,12 +452,21 @@ def get_repo_settings(self):
    except Exception:
    return ""

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    if disable_eyes:
    return None
    try:
    reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes")
    return reaction.id
    except Exception as e:
    get_logger().exception(f"Failed to add eyes reaction, error: {e}")
    try:
    headers, data_patch = self.pr._requester.requestJsonAndCheck(
    "POST", f"https://api.github.com/repos/{self.repo}/pulls/comments/{issue_comment_id}/reactions",
    input={"content": "eyes"}
    )
    except:
    pass
    return None

    def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
    Expand Down
    6 changes: 5 additions & 1 deletion pr_agent/git_providers/gitlab_provider.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -181,6 +181,10 @@ def publish_comment(self, mr_comment: str, is_temporary: bool = False):
    def edit_comment(self, comment, body: str):
    self.mr.notes.update(comment.id,{'body': body} )

    def reply_to_comment_from_comment_id(self, comment_id: int, body: str):
    discussion = self.mr.discussions.get(comment_id)
    discussion.notes.create({'body': body})

    def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
    edit_type, found, source_line_no, target_file, target_line_no = self.search_line(relevant_file,
    relevant_line_in_file)
    Expand Down Expand Up @@ -364,7 +368,7 @@ def get_repo_settings(self):
    except Exception:
    return ""

    def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
    def add_eyes_reaction(self, issue_comment_id: int, disable_eyes: bool = False) -> Optional[int]:
    return True

    def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
    Expand Down
    19 changes: 17 additions & 2 deletions pr_agent/servers/github_action_runner.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -8,6 +8,7 @@
    from pr_agent.git_providers import get_git_provider
    from pr_agent.git_providers.utils import apply_repo_settings
    from pr_agent.log import get_logger
    from pr_agent.servers.github_app import handle_line_comments
    from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
    from pr_agent.tools.pr_description import PRDescription
    from pr_agent.tools.pr_reviewer import PRReviewer
    Expand Down Expand Up @@ -102,24 +103,38 @@ async def run_action():
    await PRCodeSuggestions(pr_url).run()

    # Handle issue comment event
    elif GITHUB_EVENT_NAME == "issue_comment":
    elif GITHUB_EVENT_NAME == "issue_comment" or GITHUB_EVENT_NAME == "pull_request_review_comment":
    action = event_payload.get("action")
    if action in ["created", "edited"]:
    comment_body = event_payload.get("comment", {}).get("body")
    try:
    if GITHUB_EVENT_NAME == "pull_request_review_comment":
    if '/ask' in comment_body:
    comment_body = handle_line_comments(event_payload, comment_body)
    except Exception as e:
    get_logger().error(f"Failed to handle line comments: {e}")
    return
    if comment_body:
    is_pr = False
    disable_eyes = False
    # check if issue is pull request
    if event_payload.get("issue", {}).get("pull_request"):
    url = event_payload.get("issue", {}).get("pull_request", {}).get("url")
    is_pr = True
    elif event_payload.get("comment", {}).get("pull_request_url"): # for 'pull_request_review_comment
    url = event_payload.get("comment", {}).get("pull_request_url")
    is_pr = True
    disable_eyes = True
    else:
    url = event_payload.get("issue", {}).get("url")

    if url:
    body = comment_body.strip().lower()
    comment_id = event_payload.get("comment", {}).get("id")
    provider = get_git_provider()(pr_url=url)
    if is_pr:
    await PRAgent().handle_request(url, body, notify=lambda: provider.add_eyes_reaction(comment_id))
    await PRAgent().handle_request(url, body,
    notify=lambda: provider.add_eyes_reaction(comment_id, disable_eyes=disable_eyes))
    else:
    await PRAgent().handle_request(url, body)

    Expand Down
    28 changes: 27 additions & 1 deletion pr_agent/servers/github_app.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -96,10 +96,19 @@ async def handle_request(body: Dict[str, Any], event: str):
    get_logger().info(f"Ignoring comment from {bot_user} user")
    return {}
    get_logger().info(f"Processing comment from {sender} user")
    disable_eyes = False
    if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
    api_url = body["issue"]["pull_request"]["url"]
    elif "comment" in body and "pull_request_url" in body["comment"]:
    api_url = body["comment"]["pull_request_url"]
    try:
    if ('/ask' in comment_body and
    'subject_type' in body["comment"] and body["comment"]["subject_type"] == "line"):
    comment_body = handle_line_comments(body, comment_body)
    disable_eyes = True
    except Exception as e:
    get_logger().error(f"Failed to handle line comments: {e}")

    else:
    return {}
    log_context["api_url"] = api_url
    Expand All @@ -108,7 +117,8 @@ async def handle_request(body: Dict[str, Any], event: str):
    comment_id = body.get("comment", {}).get("id")
    provider = get_git_provider()(pr_url=api_url)
    with get_logger().contextualize(**log_context):
    await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
    await agent.handle_request(api_url, comment_body,
    notify=lambda: provider.add_eyes_reaction(comment_id, disable_eyes=disable_eyes))

    # handle pull_request event:
    # automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
    Expand Down Expand Up @@ -190,6 +200,22 @@ async def handle_request(body: Dict[str, Any], event: str):
    return {}


    def handle_line_comments(body, comment_body):
    # handle line comments
    start_line = body["comment"]["start_line"]
    end_line = body["comment"]["line"]
    start_line = end_line if not start_line else start_line
    question = comment_body.replace('/ask', '').strip()
    diff_hunk = body["comment"]["diff_hunk"]
    get_settings().set("ask_diff_hunk", diff_hunk)
    path = body["comment"]["path"]
    side = body["comment"]["side"]
    comment_id = body["comment"]["id"]
    if '/ask' in comment_body:
    comment_body = f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={comment_id} {question}"
    return comment_body


    def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_user: str) -> Tuple[Dict[str, Any], str]:
    invalid_result = {}, ""
    pull_request = body.get("pull_request")
    Expand Down
    18 changes: 18 additions & 0 deletions pr_agent/servers/gitlab_webhook.py
    Original file line number Diff line number Diff line change
    Expand Up @@ -64,7 +64,25 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
    mr = data['merge_request']
    url = mr.get('url')
    body = data.get('object_attributes', {}).get('note')
    if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body:
    Copy link
    Collaborator

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    move this to a dedicated function

    line_range_ = data['object_attributes']['position']['line_range']

    # if line_range_['start']['type'] == 'new':
    start_line = line_range_['start']['new_line']
    end_line = line_range_['end']['new_line']
    # else:
    # start_line = line_range_['start']['old_line']
    # end_line = line_range_['end']['old_line']

    question = body.replace('/ask', '').strip()
    path = data['object_attributes']['position']['new_path']
    side = 'RIGHT'# if line_range_['start']['type'] == 'new' else 'LEFT'
    comment_id = data['object_attributes']["discussion_id"]
    get_logger().info(f"Handling line comment")
    body = f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={comment_id} {question}"

    handle_request(background_tasks, url, body, log_context)

    return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))


    Expand Down
    Loading
    Loading