Spaces:
Build error
Build error
| import json | |
| import os | |
| import re | |
| from typing import Any, ClassVar | |
| import jinja2 | |
| from openhands.core.config import LLMConfig | |
| from openhands.events.event import Event | |
| from openhands.llm.llm import LLM | |
| from openhands.resolver.interfaces.issue import ( | |
| Issue, | |
| IssueHandlerInterface, | |
| ReviewThread, | |
| ) | |
| from openhands.resolver.utils import extract_image_urls | |
| class ServiceContext: | |
| issue_type: ClassVar[str] | |
| default_git_patch: ClassVar[str] = 'No changes made yet' | |
| def __init__(self, strategy: IssueHandlerInterface, llm_config: LLMConfig | None): | |
| self._strategy = strategy | |
| if llm_config is not None: | |
| self.llm = LLM(llm_config) | |
| def set_strategy(self, strategy: IssueHandlerInterface) -> None: | |
| self._strategy = strategy | |
| # Strategy context interface | |
| class ServiceContextPR(ServiceContext): | |
| issue_type: ClassVar[str] = 'pr' | |
| def __init__(self, strategy: IssueHandlerInterface, llm_config: LLMConfig): | |
| super().__init__(strategy, llm_config) | |
| def get_clone_url(self) -> str: | |
| return self._strategy.get_clone_url() | |
| def download_issues(self) -> list[Any]: | |
| return self._strategy.download_issues() | |
| def guess_success( | |
| self, | |
| issue: Issue, | |
| history: list[Event], | |
| git_patch: str | None = None, | |
| ) -> tuple[bool, None | list[bool], str]: | |
| """Guess if the issue is fixed based on the history, issue description and git patch. | |
| Args: | |
| issue: The issue to check | |
| history: The agent's history | |
| git_patch: Optional git patch showing the changes made | |
| """ | |
| last_message = history[-1].message | |
| issues_context = json.dumps(issue.closing_issues, indent=4) | |
| success_list = [] | |
| explanation_list = [] | |
| # Handle PRs with file-specific review comments | |
| if issue.review_threads: | |
| for review_thread in issue.review_threads: | |
| if issues_context and last_message: | |
| success, explanation = self._check_review_thread( | |
| review_thread, issues_context, last_message, git_patch | |
| ) | |
| else: | |
| success, explanation = False, 'Missing context or message' | |
| success_list.append(success) | |
| explanation_list.append(explanation) | |
| # Handle PRs with only thread comments (no file-specific review comments) | |
| elif issue.thread_comments: | |
| if issue.thread_comments and issues_context and last_message: | |
| success, explanation = self._check_thread_comments( | |
| issue.thread_comments, issues_context, last_message, git_patch | |
| ) | |
| else: | |
| success, explanation = ( | |
| False, | |
| 'Missing thread comments, context or message', | |
| ) | |
| success_list.append(success) | |
| explanation_list.append(explanation) | |
| elif issue.review_comments: | |
| # Handle PRs with only review comments (no file-specific review comments or thread comments) | |
| if issue.review_comments and issues_context and last_message: | |
| success, explanation = self._check_review_comments( | |
| issue.review_comments, issues_context, last_message, git_patch | |
| ) | |
| else: | |
| success, explanation = ( | |
| False, | |
| 'Missing review comments, context or message', | |
| ) | |
| success_list.append(success) | |
| explanation_list.append(explanation) | |
| else: | |
| # No review comments, thread comments, or file-level review comments found | |
| return False, None, 'No feedback was found to process' | |
| # Return overall success (all must be true) and explanations | |
| if not success_list: | |
| return False, None, 'No feedback was processed' | |
| return all(success_list), success_list, json.dumps(explanation_list) | |
| def get_converted_issues( | |
| self, issue_numbers: list[int] | None = None, comment_id: int | None = None | |
| ) -> list[Issue]: | |
| return self._strategy.get_converted_issues(issue_numbers, comment_id) | |
| def get_instruction( | |
| self, | |
| issue: Issue, | |
| user_instructions_prompt_template: str, | |
| conversation_instructions_prompt_template: str, | |
| repo_instruction: str | None = None, | |
| ) -> tuple[str, str, list[str]]: | |
| """Generate instruction for the agent.""" | |
| user_instruction_template = jinja2.Template(user_instructions_prompt_template) | |
| conversation_instructions_template = jinja2.Template( | |
| conversation_instructions_prompt_template | |
| ) | |
| images = [] | |
| issues_str = None | |
| if issue.closing_issues: | |
| issues_str = json.dumps(issue.closing_issues, indent=4) | |
| images.extend(extract_image_urls(issues_str)) | |
| # Handle PRs with review comments | |
| review_comments_str = None | |
| if issue.review_comments: | |
| review_comments_str = json.dumps(issue.review_comments, indent=4) | |
| images.extend(extract_image_urls(review_comments_str)) | |
| # Handle PRs with file-specific review comments | |
| review_thread_str = None | |
| review_thread_file_str = None | |
| if issue.review_threads: | |
| review_threads = [ | |
| review_thread.comment for review_thread in issue.review_threads | |
| ] | |
| review_thread_files = [] | |
| for review_thread in issue.review_threads: | |
| review_thread_files.extend(review_thread.files) | |
| review_thread_str = json.dumps(review_threads, indent=4) | |
| review_thread_file_str = json.dumps(review_thread_files, indent=4) | |
| images.extend(extract_image_urls(review_thread_str)) | |
| # Format thread comments if they exist | |
| thread_context = '' | |
| if issue.thread_comments: | |
| thread_context = '\n---\n'.join(issue.thread_comments) | |
| images.extend(extract_image_urls(thread_context)) | |
| user_instruction = user_instruction_template.render( | |
| review_comments=review_comments_str, | |
| review_threads=review_thread_str, | |
| files=review_thread_file_str, | |
| thread_context=thread_context, | |
| ) | |
| conversation_instructions = conversation_instructions_template.render( | |
| issues=issues_str, repo_instruction=repo_instruction | |
| ) | |
| return user_instruction, conversation_instructions, images | |
| def _check_feedback_with_llm(self, prompt: str) -> tuple[bool, str]: | |
| """Helper function to check feedback with LLM and parse response.""" | |
| response = self.llm.completion(messages=[{'role': 'user', 'content': prompt}]) | |
| answer = response.choices[0].message.content.strip() | |
| pattern = r'--- success\n*(true|false)\n*--- explanation*\n((?:.|\n)*)' | |
| match = re.search(pattern, answer) | |
| if match: | |
| return match.group(1).lower() == 'true', match.group(2).strip() | |
| return False, f'Failed to decode answer from LLM response: {answer}' | |
| def _check_review_thread( | |
| self, | |
| review_thread: ReviewThread, | |
| issues_context: str, | |
| last_message: str, | |
| git_patch: str | None = None, | |
| ) -> tuple[bool, str]: | |
| """Check if a review thread's feedback has been addressed.""" | |
| files_context = json.dumps(review_thread.files, indent=4) | |
| with open( | |
| os.path.join( | |
| os.path.dirname(__file__), | |
| '../prompts/guess_success/pr-feedback-check.jinja', | |
| ), | |
| 'r', | |
| ) as f: | |
| template = jinja2.Template(f.read()) | |
| prompt = template.render( | |
| issue_context=issues_context, | |
| feedback=review_thread.comment, | |
| files_context=files_context, | |
| last_message=last_message, | |
| git_patch=git_patch or self.default_git_patch, | |
| ) | |
| return self._check_feedback_with_llm(prompt) | |
| def _check_thread_comments( | |
| self, | |
| thread_comments: list[str], | |
| issues_context: str, | |
| last_message: str, | |
| git_patch: str | None = None, | |
| ) -> tuple[bool, str]: | |
| """Check if thread comments feedback has been addressed.""" | |
| thread_context = '\n---\n'.join(thread_comments) | |
| with open( | |
| os.path.join( | |
| os.path.dirname(__file__), | |
| '../prompts/guess_success/pr-thread-check.jinja', | |
| ), | |
| 'r', | |
| ) as f: | |
| template = jinja2.Template(f.read()) | |
| prompt = template.render( | |
| issue_context=issues_context, | |
| thread_context=thread_context, | |
| last_message=last_message, | |
| git_patch=git_patch or self.default_git_patch, | |
| ) | |
| return self._check_feedback_with_llm(prompt) | |
| def _check_review_comments( | |
| self, | |
| review_comments: list[str], | |
| issues_context: str, | |
| last_message: str, | |
| git_patch: str | None = None, | |
| ) -> tuple[bool, str]: | |
| """Check if review comments feedback has been addressed.""" | |
| review_context = '\n---\n'.join(review_comments) | |
| with open( | |
| os.path.join( | |
| os.path.dirname(__file__), | |
| '../prompts/guess_success/pr-review-check.jinja', | |
| ), | |
| 'r', | |
| ) as f: | |
| template = jinja2.Template(f.read()) | |
| prompt = template.render( | |
| issue_context=issues_context, | |
| review_context=review_context, | |
| last_message=last_message, | |
| git_patch=git_patch or self.default_git_patch, | |
| ) | |
| return self._check_feedback_with_llm(prompt) | |
| class ServiceContextIssue(ServiceContext): | |
| issue_type: ClassVar[str] = 'issue' | |
| def __init__(self, strategy: IssueHandlerInterface, llm_config: LLMConfig | None): | |
| super().__init__(strategy, llm_config) | |
| def get_base_url(self) -> str: | |
| return self._strategy.get_base_url() | |
| def get_branch_url(self, branch_name: str) -> str: | |
| return self._strategy.get_branch_url(branch_name) | |
| def get_download_url(self) -> str: | |
| return self._strategy.get_download_url() | |
| def get_clone_url(self) -> str: | |
| return self._strategy.get_clone_url() | |
| def get_graphql_url(self) -> str: | |
| return self._strategy.get_graphql_url() | |
| def get_headers(self) -> dict[str, str]: | |
| return self._strategy.get_headers() | |
| def get_authorize_url(self) -> str: | |
| return self._strategy.get_authorize_url() | |
| def get_pull_url(self, pr_number: int) -> str: | |
| return self._strategy.get_pull_url(pr_number) | |
| def get_compare_url(self, branch_name: str) -> str: | |
| return self._strategy.get_compare_url(branch_name) | |
| def download_issues(self) -> list[Any]: | |
| return self._strategy.download_issues() | |
| def get_branch_name( | |
| self, | |
| base_branch_name: str, | |
| ) -> str: | |
| return self._strategy.get_branch_name(base_branch_name) | |
| def branch_exists(self, branch_name: str) -> bool: | |
| return self._strategy.branch_exists(branch_name) | |
| def get_default_branch_name(self) -> str: | |
| return self._strategy.get_default_branch_name() | |
| def create_pull_request(self, data: dict[str, Any] | None = None) -> dict[str, Any]: | |
| if data is None: | |
| data = {} | |
| return self._strategy.create_pull_request(data) | |
| def request_reviewers(self, reviewer: str, pr_number: int) -> None: | |
| return self._strategy.request_reviewers(reviewer, pr_number) | |
| def reply_to_comment(self, pr_number: int, comment_id: str, reply: str) -> None: | |
| return self._strategy.reply_to_comment(pr_number, comment_id, reply) | |
| def send_comment_msg(self, issue_number: int, msg: str) -> None: | |
| return self._strategy.send_comment_msg(issue_number, msg) | |
| def get_issue_comments( | |
| self, issue_number: int, comment_id: int | None = None | |
| ) -> list[str] | None: | |
| return self._strategy.get_issue_comments(issue_number, comment_id) | |
| def get_instruction( | |
| self, | |
| issue: Issue, | |
| user_instructions_prompt_template: str, | |
| conversation_instructions_prompt_template: str, | |
| repo_instruction: str | None = None, | |
| ) -> tuple[str, str, list[str]]: | |
| """Generate instruction for the agent.""" | |
| # Format thread comments if they exist | |
| thread_context = '' | |
| if issue.thread_comments: | |
| thread_context = '\n\nIssue Thread Comments:\n' + '\n---\n'.join( | |
| issue.thread_comments | |
| ) | |
| images = [] | |
| images.extend(extract_image_urls(issue.body)) | |
| images.extend(extract_image_urls(thread_context)) | |
| user_instructions_template = jinja2.Template(user_instructions_prompt_template) | |
| user_instructions = user_instructions_template.render( | |
| body=issue.title + '\n\n' + issue.body + thread_context | |
| ) # Issue body and comments | |
| conversation_instructions_template = jinja2.Template( | |
| conversation_instructions_prompt_template | |
| ) | |
| conversation_instructions = conversation_instructions_template.render( | |
| repo_instruction=repo_instruction, | |
| ) | |
| return user_instructions, conversation_instructions, images | |
| def guess_success( | |
| self, issue: Issue, history: list[Event], git_patch: str | None = None | |
| ) -> tuple[bool, None | list[bool], str]: | |
| """Guess if the issue is fixed based on the history and the issue description. | |
| Args: | |
| issue: The issue to check | |
| history: The agent's history | |
| git_patch: Optional git patch showing the changes made | |
| """ | |
| last_message = history[-1].message | |
| # Include thread comments in the prompt if they exist | |
| issue_context = issue.body | |
| if issue.thread_comments: | |
| issue_context += '\n\nIssue Thread Comments:\n' + '\n---\n'.join( | |
| issue.thread_comments | |
| ) | |
| with open( | |
| os.path.join( | |
| os.path.dirname(__file__), | |
| '../prompts/guess_success/issue-success-check.jinja', | |
| ), | |
| 'r', | |
| ) as f: | |
| template = jinja2.Template(f.read()) | |
| prompt = template.render( | |
| issue_context=issue_context, | |
| last_message=last_message, | |
| git_patch=git_patch or self.default_git_patch, | |
| ) | |
| response = self.llm.completion(messages=[{'role': 'user', 'content': prompt}]) | |
| answer = response.choices[0].message.content.strip() | |
| pattern = r'--- success\n*(true|false)\n*--- explanation*\n((?:.|\n)*)' | |
| match = re.search(pattern, answer) | |
| if match: | |
| return match.group(1).lower() == 'true', None, match.group(2) | |
| return False, None, f'Failed to decode answer from LLM response: {answer}' | |
| def get_converted_issues( | |
| self, issue_numbers: list[int] | None = None, comment_id: int | None = None | |
| ) -> list[Issue]: | |
| return self._strategy.get_converted_issues(issue_numbers, comment_id) | |