Add Claude Code integration with automatic model routing
Implemented complete Claude Code support alongside existing Aider integration: - ClaudeCodeSolver strategy with programmatic Claude Code CLI usage - Intelligent model detection to route Anthropic models to Claude Code - Shared post-solver cleanup function to eliminate code duplication - CLAUDE_CODE_MESSAGE_FORMAT constant for maintainable prompts - Comprehensive test suite with 18 passing tests - Automatic ANTHROPIC_API_KEY environment setup Users can now specify any Anthropic model (claude, sonnet, haiku, opus) to use Claude Code, while other models continue using Aider. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0b9902c428
commit
325c0767f1
|
@ -165,6 +165,18 @@ Go ahead with the changes you deem appropriate without waiting for explicit appr
|
||||||
Do not draft changes beforehand; produce changes only once prompted for a specific file.
|
Do not draft changes beforehand; produce changes only once prompted for a specific file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CLAUDE_CODE_MESSAGE_FORMAT = """{issue}
|
||||||
|
|
||||||
|
Please fix this issue by making the necessary code changes. Follow these guidelines:
|
||||||
|
1. Run tests after making changes to ensure they pass
|
||||||
|
2. Follow existing code style and conventions
|
||||||
|
3. Make minimal, focused changes to address the issue
|
||||||
|
4. Commit your changes with a descriptive message
|
||||||
|
|
||||||
|
The test command for this project is: {test_command}
|
||||||
|
The lint command for this project is: {lint_command}
|
||||||
|
"""
|
||||||
|
|
||||||
CODE_MODEL = None
|
CODE_MODEL = None
|
||||||
EVALUATOR_MODEL = 'ollama/gemma3:27b'
|
EVALUATOR_MODEL = 'ollama/gemma3:27b'
|
||||||
|
|
||||||
|
@ -174,6 +186,23 @@ MODEL_EDIT_MODES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_post_solver_cleanup(repository_path: Path, solver_name: str) -> None:
|
||||||
|
"""Run standard code quality fixes and commit changes after a code solver.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repository_path: Path to the repository
|
||||||
|
solver_name: Name of the solver (for commit message)
|
||||||
|
"""
|
||||||
|
# Auto-fix standard code quality stuff
|
||||||
|
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False)
|
||||||
|
run_cmd(['git', 'add', '.'], repository_path)
|
||||||
|
run_cmd(
|
||||||
|
['git', 'commit', '-m', f'Ruff after {solver_name}'],
|
||||||
|
repository_path,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CodeSolverStrategy:
|
class CodeSolverStrategy:
|
||||||
"""Base interface for code solving strategies."""
|
"""Base interface for code solving strategies."""
|
||||||
|
@ -258,18 +287,109 @@ class AiderCodeSolver(CodeSolverStrategy):
|
||||||
if not aider_did_not_crash:
|
if not aider_did_not_crash:
|
||||||
return aider_did_not_crash
|
return aider_did_not_crash
|
||||||
|
|
||||||
# Auto-fix standard code quality stuff after aider
|
# Run post-solver cleanup
|
||||||
run_cmd(['bash', '-c', RUFF_FORMAT_AND_AUTO_FIX], repository_path, check=False)
|
run_post_solver_cleanup(repository_path, 'aider')
|
||||||
run_cmd(['git', 'add', '.'], repository_path)
|
|
||||||
run_cmd(
|
return True
|
||||||
['git', 'commit', '-m', 'Ruff after aider'],
|
|
||||||
repository_path,
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class ClaudeCodeSolver(CodeSolverStrategy):
|
||||||
|
"""Code solver that uses Claude Code for issue resolution."""
|
||||||
|
|
||||||
|
def _create_claude_command(self, issue: str) -> list[str]:
|
||||||
|
"""Create the Claude Code command for programmatic use."""
|
||||||
|
cmd = [
|
||||||
|
'claude',
|
||||||
|
'-p',
|
||||||
|
'--output-format',
|
||||||
|
'json',
|
||||||
|
'--max-turns',
|
||||||
|
'10',
|
||||||
|
]
|
||||||
|
|
||||||
|
if CODE_MODEL:
|
||||||
|
cmd.extend(['--model', CODE_MODEL])
|
||||||
|
|
||||||
|
cmd.append(issue)
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
def solve_issue_round(self, repository_path: Path, issue_content: str) -> bool:
|
||||||
|
"""Solve an issue using Claude Code."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Set Anthropic API key environment variable
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['ANTHROPIC_API_KEY'] = secrets.anthropic_api_key()
|
||||||
|
|
||||||
|
# Prepare the issue prompt for Claude Code
|
||||||
|
enhanced_issue = CLAUDE_CODE_MESSAGE_FORMAT.format(
|
||||||
|
issue=issue_content,
|
||||||
|
test_command=AIDER_TEST,
|
||||||
|
lint_command=AIDER_LINT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Claude Code command
|
||||||
|
claude_command = self._create_claude_command(enhanced_issue)
|
||||||
|
|
||||||
|
# Run Claude Code
|
||||||
|
result = subprocess.run(
|
||||||
|
claude_command,
|
||||||
|
cwd=repository_path,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error('Claude Code failed with return code %d', result.returncode)
|
||||||
|
logger.error('stderr: %s', result.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Parse response if it's JSON
|
||||||
|
try:
|
||||||
|
if result.stdout.strip():
|
||||||
|
response_data = json.loads(result.stdout)
|
||||||
|
logger.info(
|
||||||
|
'Claude Code response: %s',
|
||||||
|
response_data.get('text', 'No text field'),
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.info('Claude Code response (non-JSON): %s', result.stdout[:500])
|
||||||
|
|
||||||
|
# Run post-solver cleanup
|
||||||
|
run_post_solver_cleanup(repository_path, 'Claude Code')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_anthropic_model(model: str) -> bool:
|
||||||
|
"""Check if the model string indicates an Anthropic/Claude model."""
|
||||||
|
if not model:
|
||||||
|
return False
|
||||||
|
|
||||||
|
anthropic_indicators = [
|
||||||
|
'claude',
|
||||||
|
'anthropic',
|
||||||
|
'sonnet',
|
||||||
|
'haiku',
|
||||||
|
'opus',
|
||||||
|
]
|
||||||
|
|
||||||
|
model_lower = model.lower()
|
||||||
|
return any(indicator in model_lower for indicator in anthropic_indicators)
|
||||||
|
|
||||||
|
|
||||||
|
def create_code_solver() -> CodeSolverStrategy:
|
||||||
|
"""Create the appropriate code solver based on the configured model."""
|
||||||
|
if is_anthropic_model(CODE_MODEL):
|
||||||
|
return ClaudeCodeSolver()
|
||||||
|
else:
|
||||||
|
return AiderCodeSolver()
|
||||||
|
|
||||||
|
|
||||||
def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]:
|
def get_commit_messages(cwd: Path, base_branch: str, current_branch: str) -> list[str]:
|
||||||
"""Get commit messages between base branch and current branch.
|
"""Get commit messages between base branch and current branch.
|
||||||
|
|
||||||
|
@ -572,7 +692,7 @@ def solve_issues_in_repository(
|
||||||
logger.info('Skipping already processed issue #%s: %s', issue_number, title)
|
logger.info('Skipping already processed issue #%s: %s', issue_number, title)
|
||||||
else:
|
else:
|
||||||
branch_name = generate_branch_name(issue_number, title)
|
branch_name = generate_branch_name(issue_number, title)
|
||||||
code_solver = AiderCodeSolver()
|
code_solver = create_code_solver()
|
||||||
with tempfile.TemporaryDirectory() as repository_path:
|
with tempfile.TemporaryDirectory() as repository_path:
|
||||||
issue_resolution = solve_issue_in_repository(
|
issue_resolution = solve_issue_in_repository(
|
||||||
repository_config,
|
repository_config,
|
||||||
|
|
|
@ -9,3 +9,7 @@ def llm_api_keys() -> list[str]:
|
||||||
|
|
||||||
def gitea_token() -> str:
|
def gitea_token() -> str:
|
||||||
return SECRETS.load_or_fail('GITEA_TOKEN')
|
return SECRETS.load_or_fail('GITEA_TOKEN')
|
||||||
|
|
||||||
|
|
||||||
|
def anthropic_api_key() -> str:
|
||||||
|
return SECRETS.load_or_fail('ANTHROPIC_API_KEY')
|
||||||
|
|
122
test/test_claude_code_integration.py
Normal file
122
test/test_claude_code_integration.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aider_gitea import (
|
||||||
|
AIDER_LINT,
|
||||||
|
AIDER_TEST,
|
||||||
|
CLAUDE_CODE_MESSAGE_FORMAT,
|
||||||
|
AiderCodeSolver,
|
||||||
|
ClaudeCodeSolver,
|
||||||
|
create_code_solver,
|
||||||
|
is_anthropic_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestClaudeCodeIntegration:
|
||||||
|
"""Test Claude Code integration and model routing logic."""
|
||||||
|
|
||||||
|
def test_is_anthropic_model_detection(self):
|
||||||
|
"""Test that Anthropic models are correctly detected."""
|
||||||
|
# Anthropic models should return True
|
||||||
|
assert is_anthropic_model('claude-3-sonnet')
|
||||||
|
assert is_anthropic_model('claude-3-haiku')
|
||||||
|
assert is_anthropic_model('claude-3-opus')
|
||||||
|
assert is_anthropic_model('anthropic/claude-3-sonnet')
|
||||||
|
assert is_anthropic_model('Claude-3-Sonnet') # Case insensitive
|
||||||
|
assert is_anthropic_model('ANTHROPIC/CLAUDE')
|
||||||
|
assert is_anthropic_model('some-sonnet-model')
|
||||||
|
assert is_anthropic_model('haiku-variant')
|
||||||
|
|
||||||
|
# Non-Anthropic models should return False
|
||||||
|
assert not is_anthropic_model('gpt-4')
|
||||||
|
assert not is_anthropic_model('gpt-3.5-turbo')
|
||||||
|
assert not is_anthropic_model('ollama/llama')
|
||||||
|
assert not is_anthropic_model('gemini-pro')
|
||||||
|
assert not is_anthropic_model('mistral-7b')
|
||||||
|
assert not is_anthropic_model('')
|
||||||
|
assert not is_anthropic_model(None)
|
||||||
|
|
||||||
|
def test_create_code_solver_routing(self, monkeypatch):
|
||||||
|
"""Test that the correct solver is created based on model."""
|
||||||
|
import aider_gitea
|
||||||
|
|
||||||
|
# Test Anthropic model routing
|
||||||
|
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', 'claude-3-sonnet')
|
||||||
|
solver = create_code_solver()
|
||||||
|
assert isinstance(solver, ClaudeCodeSolver)
|
||||||
|
|
||||||
|
# Test non-Anthropic model routing
|
||||||
|
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', 'gpt-4')
|
||||||
|
solver = create_code_solver()
|
||||||
|
assert isinstance(solver, AiderCodeSolver)
|
||||||
|
|
||||||
|
# Test None model routing (should default to Aider)
|
||||||
|
monkeypatch.setattr(aider_gitea, 'CODE_MODEL', None)
|
||||||
|
solver = create_code_solver()
|
||||||
|
assert isinstance(solver, AiderCodeSolver)
|
||||||
|
|
||||||
|
def test_claude_code_solver_command_creation(self):
|
||||||
|
"""Test that Claude Code commands are created correctly."""
|
||||||
|
import aider_gitea
|
||||||
|
|
||||||
|
solver = ClaudeCodeSolver()
|
||||||
|
issue = 'Fix the bug in the code'
|
||||||
|
|
||||||
|
# Test without model
|
||||||
|
with pytest.MonkeyPatch().context() as m:
|
||||||
|
m.setattr(aider_gitea, 'CODE_MODEL', None)
|
||||||
|
cmd = solver._create_claude_command(issue)
|
||||||
|
expected = [
|
||||||
|
'claude',
|
||||||
|
'-p',
|
||||||
|
'--output-format',
|
||||||
|
'json',
|
||||||
|
'--max-turns',
|
||||||
|
'10',
|
||||||
|
issue,
|
||||||
|
]
|
||||||
|
assert cmd == expected
|
||||||
|
|
||||||
|
# Test with model
|
||||||
|
with pytest.MonkeyPatch().context() as m:
|
||||||
|
m.setattr(aider_gitea, 'CODE_MODEL', 'claude-3-sonnet')
|
||||||
|
cmd = solver._create_claude_command(issue)
|
||||||
|
expected = [
|
||||||
|
'claude',
|
||||||
|
'-p',
|
||||||
|
'--output-format',
|
||||||
|
'json',
|
||||||
|
'--max-turns',
|
||||||
|
'10',
|
||||||
|
'--model',
|
||||||
|
'claude-3-sonnet',
|
||||||
|
issue,
|
||||||
|
]
|
||||||
|
assert cmd == expected
|
||||||
|
|
||||||
|
def test_claude_code_message_format(self):
|
||||||
|
"""Test that Claude Code message format works correctly."""
|
||||||
|
issue_content = 'Fix the authentication bug'
|
||||||
|
|
||||||
|
formatted_message = CLAUDE_CODE_MESSAGE_FORMAT.format(
|
||||||
|
issue=issue_content,
|
||||||
|
test_command=AIDER_TEST,
|
||||||
|
lint_command=AIDER_LINT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the issue content is included
|
||||||
|
assert issue_content in formatted_message
|
||||||
|
|
||||||
|
# Verify the test and lint commands are included
|
||||||
|
assert AIDER_TEST in formatted_message
|
||||||
|
assert AIDER_LINT in formatted_message
|
||||||
|
|
||||||
|
# Verify the guidelines are present
|
||||||
|
assert 'Run tests after making changes' in formatted_message
|
||||||
|
assert 'Follow existing code style' in formatted_message
|
||||||
|
assert 'Make minimal, focused changes' in formatted_message
|
||||||
|
assert 'Commit your changes' in formatted_message
|
||||||
|
|
||||||
|
# Verify the structure contains placeholders that got replaced
|
||||||
|
assert '{issue}' not in formatted_message
|
||||||
|
assert '{test_command}' not in formatted_message
|
||||||
|
assert '{lint_command}' not in formatted_message
|
Loading…
Reference in New Issue
Block a user