diff --git a/aider_gitea/__init__.py b/aider_gitea/__init__.py index e8e9c8f..52187de 100644 --- a/aider_gitea/__init__.py +++ b/aider_gitea/__init__.py @@ -134,13 +134,13 @@ def bash_cmd(*commands: str) -> str: AIDER_TEST = bash_cmd( - 'echo "Setting up virtual environment"' + 'echo "Setting up virtual environment"', 'virtualenv venv', - 'echo "Activating virtual environment"' + 'echo "Activating virtual environment"', 'source venv/bin/activate', - 'echo "Installing package"' + 'echo "Installing package"', 'pip install -e .', - 'echo "Testing package"' + 'echo "Testing package"', 'pytest test', ) @@ -169,61 +169,105 @@ CODE_MODEL = None EVALUATOR_MODEL = 'ollama/gemma3:27b' MODEL_EDIT_MODES = { - 'ollama/qwen3:32b': 'diff', - 'ollama/hf.co/unsloth/Qwen3-30B-A3B-GGUF:Q4_K_M': 'diff', + 'ollama/qwen3:32b': 'diff', + 'ollama/hf.co/unsloth/Qwen3-30B-A3B-GGUF:Q4_K_M': 'diff', } -def create_aider_command(issue: str) -> list[str]: - l = [ - 'aider', - '--chat-language', - 'english', - '--no-stream', - '--no-analytics', - #'--no-check-update', - '--test-cmd', - AIDER_TEST, - '--lint-cmd', - AIDER_LINT, - '--auto-test', - '--no-auto-lint', - '--yes', - '--disable-playwright', - '--timeout', - str(10_000), - ] +@dataclasses.dataclass(frozen=True) +class CodeSolverStrategy: + """Base interface for code solving strategies.""" - if edit_format := MODEL_EDIT_MODES.get(CODE_MODEL): - l.append('--edit-format') - l.append(edit_format) - del edit_format + def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool: + """Attempt to solve an issue in a single round. - for key in secrets.llm_api_keys(): - l += ['--api-key', key] + Args: + repository_path: Path to the repository + issue_content: The issue description to solve - if False: - l.append('--read') - l.append('CONVENTIONS.md') + Returns: + True if the solution round completed without crashing, False otherwise + """ + raise NotImplementedError - if True: - l.append('--cache-prompts') - if False: - l.append('--architect') +@dataclasses.dataclass(frozen=True) +class AiderCodeSolver(CodeSolverStrategy): + """Code solver that uses Aider for issue resolution.""" - if CODE_MODEL: - l.append('--model') - l.append(CODE_MODEL) + def _create_aider_command(self, issue: str) -> list[str]: + """Create the Aider command with all necessary flags.""" + l = [ + 'aider', + '--chat-language', + 'english', + '--no-stream', + '--no-analytics', + '--test-cmd', + AIDER_TEST, + '--lint-cmd', + AIDER_LINT, + '--auto-test', + '--no-auto-lint', + '--yes', + '--disable-playwright', + '--timeout', + str(10_000), + ] - if CODE_MODEL.startswith('ollama/') and False: - l.append('--auto-lint') + if edit_format := MODEL_EDIT_MODES.get(CODE_MODEL): + l.append('--edit-format') + l.append(edit_format) + del edit_format - if True: - l.append('--message') - l.append(LLM_MESSAGE_FORMAT.format(issue=issue)) + for key in secrets.llm_api_keys(): + l += ['--api-key', key] - return l + if False: + l.append('--read') + l.append('CONVENTIONS.md') + + if True: + l.append('--cache-prompts') + + if False: + l.append('--architect') + + if CODE_MODEL: + l.append('--model') + l.append(CODE_MODEL) + + if CODE_MODEL.startswith('ollama/') and False: + l.append('--auto-lint') + + if True: + l.append('--message') + l.append(LLM_MESSAGE_FORMAT.format(issue=issue)) + + return l + + def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool: + """Solve an issue using Aider.""" + # Primary Aider command + aider_command = self._create_aider_command(issue_content) + aider_did_not_crash = run_cmd( + aider_command, + repository_path, + check=False, + ) + if not aider_did_not_crash: + return aider_did_not_crash + + # Auto-fix standard code quality stuff after aider + run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False) + run_cmd(['git', 'add', '.'], repository_path) + run_cmd( + ['git', 'commit', '-m', 'Ruff after aider'], + repository_path, + check=False, + ) + + return True def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]: @@ -345,33 +389,17 @@ def run_cmd(cmd: list[str], cwd: Path | None = None, check=True) -> bool: return result.returncode == 0 -def issue_solution_round(repository_path, issue_content): - # Primary Aider command - aider_command = create_aider_command(issue_content) - aider_did_not_crash = run_cmd( - aider_command, - repository_path, - check=False, - ) - if not aider_did_not_crash: - return aider_did_not_crash - - # Auto-fix standard code quality stuff after aider - run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False) - run_cmd(['git', 'add', '.'], repository_path) - run_cmd(['git', 'commit', '-m', 'Ruff after aider'], repository_path, check=False) - - return True - def remove_thinking_tokens(text: str) -> str: text = re.sub(r'^\s*.*?', '', text, flags=re.MULTILINE | re.DOTALL) text = text.strip() return text + assert remove_thinking_tokens('Hello\nWorld\n') == 'World' assert remove_thinking_tokens('\nHello\n\nWorld\n') == 'World' assert remove_thinking_tokens('\n\nHello\n\nWorld\n') == 'World' + def run_ollama(cwd: Path, texts: list[str]) -> str: cmd = ['ollama', 'run', EVALUATOR_MODEL.removeprefix('ollama/')] process = subprocess.Popen( @@ -397,9 +425,11 @@ def parse_yes_no_answer(text: str) -> bool | None: return False return None + assert parse_yes_no_answer('Yes.') == True assert parse_yes_no_answer('no') == False + def run_ollama_and_get_yes_or_no(cwd, initial_texts: list[str]) -> bool: texts = list(initial_texts) texts.append('Think through your answer.') @@ -455,6 +485,7 @@ def solve_issue_in_repository( issue_description: str, issue_number: str, gitea_client, + code_solver: CodeSolverStrategy, ) -> IssueResolution: logger.info('### %s #####', issue_title) @@ -464,28 +495,31 @@ def solve_issue_in_repository( run_cmd(['git', 'checkout', repository_config.base_branch], repository_path) run_cmd(['git', 'checkout', '-b', branch_name], repository_path) - # Run initial ruff pass before aider + # Run initial ruff pass before code solver run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False) run_cmd(['git', 'add', '.'], repository_path) run_cmd(['git', 'commit', '-m', 'Initial ruff pass'], repository_path, check=False) - # Run aider + # Run code solver issue_content = f'# {issue_title}\n{issue_description}' while True: - # Save the commit hash after ruff but before aider + # Save the commit hash after ruff but before code solver pre_aider_commit = get_head_commit_hash(repository_path) - # Run aider - aider_did_not_crash = issue_solution_round(repository_path, issue_content) - if not aider_did_not_crash: - logger.error('Aider invocation failed for issue #%s', issue_number) + # Run code solver + solver_did_not_crash = code_solver.solve_issue_round( + repository_path, + issue_content, + ) + if not solver_did_not_crash: + logger.error('Code solver invocation failed for issue #%s', issue_number) return IssueResolution(False) - # Check if aider made any changes beyond the initial ruff pass + # Check if solver made any changes beyond the initial ruff pass if not has_commits_on_branch(repository_path, pre_aider_commit, 'HEAD'): logger.error( - 'Aider did not make any changes beyond the initial ruff pass for issue #%s', + 'Code solver did not make any changes beyond the initial ruff pass for issue #%s', issue_number, ) return IssueResolution(False) @@ -538,6 +572,7 @@ def solve_issues_in_repository( logger.info('Skipping already processed issue #%s: %s', issue_number, title) else: branch_name = generate_branch_name(issue_number, title) + code_solver = AiderCodeSolver() with tempfile.TemporaryDirectory() as repository_path: issue_resolution = solve_issue_in_repository( repository_config, @@ -547,6 +582,7 @@ def solve_issues_in_repository( issue_description, issue_number, client, + code_solver, ) seen_issues_db.mark_as_seen(issue_url, str(issue_number)) seen_issues_db.update_pr_info( @@ -571,6 +607,7 @@ def solve_issues_in_repository( client, seen_issues_db, issue_url, + code_solver, ) # Handle failing pipelines @@ -580,6 +617,7 @@ def solve_issues_in_repository( branch_name, Path(repository_path), client, + code_solver, ) @@ -591,8 +629,9 @@ def handle_pr_comments( client, seen_issues_db, issue_url, + code_solver: CodeSolverStrategy, ): - """Fetch unresolved PR comments and resolve them via aider.""" + """Fetch unresolved PR comments and resolve them via code solver.""" comments = client.get_pull_request_comments( repository_config.owner, repository_config.repo, @@ -614,8 +653,8 @@ def handle_pr_comments( f'Resolve the following reviewer comment:\n{body}\n\n' f'File: {path}\n\nContext:\n{context}' ) - # invoke aider on the comment context - issue_solution_round(repository_path, issue) + # invoke code solver on the comment context + code_solver.solve_issue_round(repository_path, issue) # commit and push changes for this comment run_cmd(['git', 'add', path], repository_path, check=False) run_cmd( @@ -632,8 +671,9 @@ def handle_failing_pipelines( branch_name: str, repository_path: Path, client, + code_solver: CodeSolverStrategy, ) -> None: - """Fetch failing pipelines for the given PR and resolve them via aider.""" + """Fetch failing pipelines for the given PR and resolve them via code solver.""" while True: failed_runs = client.get_failed_pipelines( repository_config.owner, @@ -651,7 +691,7 @@ def handle_failing_pipelines( lines = log.strip().split('\n') context = '\n'.join(lines[-100:]) issue = f'Resolve the following failing pipeline run {run_id}:\n\n{context}' - issue_solution_round(repository_path, issue) + code_solver.solve_issue_round(repository_path, issue) run_cmd(['git', 'add', '.'], repository_path, check=False) run_cmd( ['git', 'commit', '-m', f'Resolve pipeline {run_id}'], diff --git a/setup.py b/setup.py index a5b4959..84e20e2 100644 --- a/setup.py +++ b/setup.py @@ -109,6 +109,7 @@ def find_python_packages() -> list[str]: print(f'Found following packages: {packages}') return sorted(packages) + with open(PACKAGE_NAME + '/_version.py') as f: version = parse_version_file(f.read())