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.
|
||||
"""
|
||||
|
||||
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
|
||||
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)
|
||||
class CodeSolverStrategy:
|
||||
"""Base interface for code solving strategies."""
|
||||
|
@ -258,18 +287,109 @@ class AiderCodeSolver(CodeSolverStrategy):
|
|||
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,
|
||||
# Run post-solver cleanup
|
||||
run_post_solver_cleanup(repository_path, 'aider')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
"""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)
|
||||
else:
|
||||
branch_name = generate_branch_name(issue_number, title)
|
||||
code_solver = AiderCodeSolver()
|
||||
code_solver = create_code_solver()
|
||||
with tempfile.TemporaryDirectory() as repository_path:
|
||||
issue_resolution = solve_issue_in_repository(
|
||||
repository_config,
|
||||
|
|
|
@ -9,3 +9,7 @@ def llm_api_keys() -> list[str]:
|
|||
|
||||
def gitea_token() -> str:
|
||||
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