Skip to content

Improvement: Enhance ask_line tool by adding PR review comment threads as context #1687

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

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
3 changes: 3 additions & 0 deletions pr_agent/git_providers/git_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ def get_issue_comments(self):

def get_comment_url(self, comment) -> str:
return ""

def get_review_thread_comments(self, comment_id: int) -> list[dict]:
pass

#### labels operations ####
@abstractmethod
Expand Down
36 changes: 35 additions & 1 deletion pr_agent/git_providers/github_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,41 @@ def publish_inline_comments(self, comments: list[dict], disable_fallback: bool =
self._publish_inline_comments_fallback_with_verification(comments)
except Exception as e:
get_logger().error(f"Failed to publish inline code comments fallback, error: {e}")
raise e
raise e

def get_review_thread_comments(self, comment_id: int) -> list[dict]:
"""
Retrieves all comments in the same thread as the given comment.

Args:
comment_id: Review comment ID

Returns:
List of comments in the same thread
"""
try:
# Fetch all comments with a single API call
all_comments = list(self.pr.get_comments())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here, you're collecting all PR comments. You can expect to find the given comment_id within this list, making the comment = self.pr.get_comment(comment_id) code line redundant and reducing the number of API calls by half, from 2 to 1.


# Find the target comment by ID
target_comment = next((c for c in all_comments if c.id == comment_id), None)
if not target_comment:
return []

# Get root comment id
root_comment_id = target_comment.raw_data.get("in_reply_to_id", target_comment.id)
# Build the thread - include the root comment and all replies to it
thread_comments = [
c for c in all_comments if
c.id == root_comment_id or c.raw_data.get("in_reply_to_id") == root_comment_id
]


return thread_comments

except Exception as e:
get_logger().exception(f"Failed to get review comments for an inline ask command", artifact={"comment_id": comment_id, "error": e})
return []

def _publish_inline_comments_fallback_with_verification(self, comments: list[dict]):
"""
Expand Down
1 change: 1 addition & 0 deletions pr_agent/settings/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async_ai_calls=true

[pr_questions] # /ask #
enable_help_text=false
use_conversation_history=true


[pr_code_suggestions] # /improve #
Expand Down
13 changes: 13 additions & 0 deletions pr_agent/settings/pr_line_questions_prompts.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ Now focus on the selected lines from the hunk:
======
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines

{%- if conversation_history %}

Previous discussion on this code:
======
{{ conversation_history|trim }}
======

Consider this conversation history (format: "N. Username: Message", where numbers indicate the comment order). When responding:
- Maintain consistency with previous technical explanations
- Address unresolved issues from earlier discussions
- Build upon existing knowledge without contradictions
- Incorporate relevant context while focusing on the current question
{%- endif %}

A question about the selected lines:
======
Expand Down
57 changes: 56 additions & 1 deletion pr_agent/tools/pr_line_questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.git_providers.github_provider import GithubProvider
from pr_agent.log import get_logger
from pr_agent.servers.help import HelpMessage


class PR_LineQuestions:
def __init__(self, pr_url: str, args=None, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.question_str = self.parse_args(args)
Expand All @@ -35,6 +35,7 @@ def __init__(self, pr_url: str, args=None, ai_handler: partial[BaseAiHandler,] =
"question": self.question_str,
"full_hunk": "",
"selected_lines": "",
"conversation_history": "",
}
self.token_handler = TokenHandler(self.git_provider.pr,
self.vars,
Expand All @@ -56,6 +57,12 @@ async def run(self):
# if get_settings().config.publish_output:
# self.git_provider.publish_comment("Preparing answer...", is_temporary=True)

# set conversation history if enabled
# currently only supports GitHub provider
if get_settings().pr_questions.use_conversation_history and isinstance(self.git_provider, GithubProvider):
conversation_history = self._load_conversation_history()
self.vars["conversation_history"] = conversation_history

self.patch_with_lines = ""
ask_diff = get_settings().get('ask_diff_hunk', "")
line_start = get_settings().get('line_start', '')
Expand Down Expand Up @@ -92,6 +99,54 @@ async def run(self):
self.git_provider.publish_comment(model_answer_sanitized)

return ""

def _load_conversation_history(self) -> str:
"""Generate conversation history from the code review thread

Returns:
str: The formatted conversation history
"""
comment_id = get_settings().get('comment_id', '')
file_path = get_settings().get('file_name', '')
line_number = get_settings().get('line_end', '')

# early return if any required parameter is missing
if not all([comment_id, file_path, line_number]):
get_logger().error("Missing required parameters for conversation history")
return ""

try:
# retrieve thread comments
thread_comments = self.git_provider.get_review_thread_comments(comment_id)

# filter and prepare comments
filtered_comments = []
for comment in thread_comments:
body = getattr(comment, 'body', '')

# skip empty comments, current comment(will be added as a question at prompt)
if not body or not body.strip() or comment_id == comment.id:
continue

user = comment.user
author = user.login if hasattr(user, 'login') else 'Unknown'
filtered_comments.append((author, body))

# transform conversation history to string using the same pattern as get_commit_messages
if filtered_comments:
comment_count = len(filtered_comments)
get_logger().info(f"Loaded {comment_count} comments from the code review thread")

# Format as numbered list, similar to get_commit_messages
conversation_history_str = "\n".join([f"{i + 1}. {author}: {body}"
for i, (author, body) in enumerate(filtered_comments)])
return conversation_history_str

return ""

except Exception as e:
get_logger().error(f"Error processing conversation history, error: {e}")
return ""

async def _get_prediction(self, model: str):
variables = copy.deepcopy(self.vars)
Expand Down