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())