import asyncio from app.core.llm import call_llm_for_json from app.core.logger import get_logger from app.core.prompts import AUTO_FIX_PROMPT, CODE_REVIEW_PROMPT from app.models.repository import RepositoryMetadata from app.models.review import AutoFix, ReviewSuggestions from app.tools.file_scanner import read_source_samples, get_python_files from app.tools.github_tool import get_changed_files import os logger = get_logger(__name__) async def run(local_path: str, metadata: RepositoryMetadata) -> ReviewSuggestions: """ Perform AI-powered code review on repository. """ logger.info("Starting Code Review Agent", extra={"path": local_path}) # Step 1 - Get Python files base_sha = metadata.base_sha head_sha = metadata.head_sha changed_files = [] if base_sha and head_sha: changed_files = get_changed_files(local_path, base_sha, head_sha) #Step 2 - Read source code samples all_files = changed_files or [ f for f in get_python_files(local_path) if not os.path.basename(f).startswith("test_") and not os.path.basename(f).endswith("_test.py") ] source_samples = read_source_samples( local_path, max_files=4, max_chars=2500, file_list=all_files, ) if not source_samples: return ReviewSuggestions( summary="No source files found to review.", overall_score=0.0 ) # Step 3 - Call LLM and parse JSON in one shot source_code = "\n\n".join(source_samples) prompt = CODE_REVIEW_PROMPT.format(source_code=source_code) llm_data = await call_llm_for_json(prompt, task="code_review") review = ReviewSuggestions( solid_violations=llm_data.get("solid_violations", []), duplicate_code=llm_data.get("duplicate_code", []), refactor_suggestions=llm_data.get("refactor_suggestions", []), overall_score=float(llm_data.get("overall_score", 5.0)), summary=llm_data.get("summary", ""), ) # Generate auto-fixes for top 3 violations (cap LLM calls) auto_fixes: list[AutoFix] = [] top_findings = (review.solid_violations + review.refactor_suggestions)[:3] if top_findings and source_samples: first_file_content = source_samples[0] # already read fix_tasks = [ call_llm_for_json( AUTO_FIX_PROMPT.format( source_code=first_file_content[:1500], finding=finding ) ) for finding in top_findings ] fix_results = await asyncio.gather(*fix_tasks, return_exceptions=True) for res in fix_results: if isinstance(res, dict) and res.get("fixed_snippet"): auto_fixes.append(AutoFix(**res)) review = review.model_copy(update={"auto_fixes": auto_fixes}) # Second pass — if score is poor, ask LLM for a prioritised fix list if review.overall_score < 5.0: logger.info( "Low score detected, running focused follow-up pass", extra={"score": review.overall_score}, ) followup_prompt = ( f"The following code scored {review.overall_score}/10 in a review.\n\n" f"Top violations: {review.solid_violations[:3]}\n\n" f'Return ONLY a JSON object with one key: "priority_fixes": a list of up to 3 strings, ' f"each describing the single most impactful change to make first." ) followup_data = await call_llm_for_json(followup_prompt) priority_fixes = followup_data.get("priority_fixes", []) if isinstance(priority_fixes, list) and priority_fixes: review = review.model_copy( update={ "refactor_suggestions": priority_fixes + review.refactor_suggestions } ) logger.info("Code review complete", extra={"score": review.overall_score}) return review