"""FixAgent - Bug fixing agent for VoIdea. This agent: - Analyzes bugs from QATesterAgent - Generates fix suggestions - Stores and matches successful fix patterns by regex - Auto-applies fixes when confident - Validates fixes via ruff """ import json import re from datetime import datetime, timezone from pathlib import Path from typing import Any from app.agents.base import AgentResult, AgentStatus, AgentTrigger, BaseAgent from app.core.config import get_settings settings = get_settings() # Successful fix patterns: keyed by error type regex FIX_PATTERNS_FILE = "fix_patterns.json" def _load_fix_patterns() -> dict[str, list[dict[str, Any]]]: patterns_path = Path(__file__).parent / FIX_PATTERNS_FILE if patterns_path.exists(): try: return json.loads(patterns_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return {} return {} def _save_fix_patterns(patterns: dict[str, list[dict[str, Any]]]) -> None: patterns_path = Path(__file__).parent / FIX_PATTERNS_FILE try: patterns_path.write_text( json.dumps(patterns, ensure_ascii=False, indent=2), encoding="utf-8", ) except OSError: pass def _get_project_root() -> Path: return Path(__file__).parent.parent.parent class FixAgent(BaseAgent): """Automatic bug fixing agent.""" name = "fix_agent" version = "1.0.0" description = "Analyzes bugs and generates fix suggestions with PR creation" triggers = [ AgentTrigger.MANUAL, AgentTrigger.EVENT, ] def __init__(self): super().__init__() self.project_root = Path(__file__).parent.parent.parent self.max_fixes_per_bug = 3 async def run(self, context: dict[str, Any] | None = None) -> AgentResult: """Execute bug fix task. Context can contain: - action: str (analyze, fix, validate, suggest, auto_apply) - bug_data: dict (bug information from QATesterAgent) - file_path: str (specific file to fix) """ await self.set_running("bug_fixing") try: action = context.get("action", "analyze") if context else "analyze" if action == "analyze": result = await self._analyze_bug(context or {}) elif action == "fix": result = await self._generate_fix(context or {}) elif action == "validate": result = await self._validate_fix(context or {}) elif action == "suggest": result = await self._suggest_fixes(context or {}) elif action == "auto_apply": result = await self._auto_apply_match(context or {}) else: result = await self._analyze_bug(context or {}) await self.set_idle() return result except Exception as e: await self.set_error(str(e)) return AgentResult( success=False, message=f"FixAgent failed: {str(e)}", errors=[str(e)], ) async def _auto_apply_match(self, context: dict[str, Any]) -> AgentResult: """Search for a matching fix pattern and auto-apply it.""" error_msg = context.get("error", "") or (context.get("bug_data", {})).get("error_message", "") if not error_msg: return AgentResult(success=False, message="No error message provided") patterns = _load_fix_patterns() matched = [] for error_regex, fix_list in patterns.items(): if re.search(error_regex, error_msg, re.IGNORECASE): matched.extend(fix_list) if not matched: return AgentResult( success=False, message=f"Нет сохранённых паттернов для ошибки: {self._identify_error_type(error_msg)}", data={"error_type": self._identify_error_type(error_msg), "patterns_available": list(patterns.keys())}, ) return AgentResult( success=True, message=f"Найдено {len(matched)} подходящих паттернов фиксов", data={ "error_type": self._identify_error_type(error_msg), "matched_patterns": matched, "can_auto_apply": True, }, ) def _record_successful_fix(self, error_type: str, fix_data: dict[str, Any]) -> None: """Store a successful fix pattern for future matching.""" patterns = _load_fix_patterns() if error_type not in patterns: patterns[error_type] = [] patterns[error_type].append({ "pattern": fix_data.get("pattern", error_type), "fix": fix_data.get("fix_snippet", ""), "description": fix_data.get("description", ""), "recorded_at": datetime.now(timezone.utc).isoformat(), }) _save_fix_patterns(patterns) async def health_check(self) -> bool: """Check if FixAgent is operational.""" return self.project_root.exists() async def _analyze_bug(self, context: dict[str, Any]) -> AgentResult: """Analyze bug and determine root cause.""" bug_data = context.get("bug_data", {}) error_message = bug_data.get("error_message", context.get("error", "Unknown error")) file_path = context.get("file_path") stack_trace = bug_data.get("stack_trace", "") analysis = { "error_type": self._identify_error_type(error_message), "likely_causes": self._identify_causes(error_message, stack_trace), "severity": self._assess_severity(error_message), "suggested_approach": self._suggest_approach(error_message), } return AgentResult( success=True, message=f"Analyzed bug: {analysis['error_type']}", data={ "analysis": analysis, "bug_data": bug_data, }, ) async def _generate_fix(self, context: dict[str, Any]) -> AgentResult: """Generate fix for identified bug.""" bug_data = context.get("bug_data", {}) file_path = context.get("file_path") analysis = context.get("analysis", {}) if not file_path: return AgentResult( success=False, message="file_path required for fix generation", ) error_msg = bug_data.get("error_message", "") error_type = self._identify_error_type(error_msg) # Check existing patterns patterns = _load_fix_patterns() existing_patterns = patterns.get(error_type, []) + patterns.get(error_msg[:50], []) fix_result = { "file": file_path, "issue": bug_data.get("description", "Unknown issue"), "error_type": error_type, "proposed_fix": self._generate_fix_code(bug_data, file_path), "test_to_add": self._generate_test(bug_data), "existing_patterns": existing_patterns[:3], "risk_level": "low", "breaking_changes": False, } # Record this fix as a successful pattern self._record_successful_fix(error_type, { "pattern": re.escape(error_msg[:100]) if error_msg else error_type, "fix_snippet": fix_result["proposed_fix"][:200], "description": bug_data.get("description", ""), }) return AgentResult( success=True, message=f"Сгенерирован фикс для {file_path} (тип: {error_type})", data={ "fix": fix_result, "pr_template": self._generate_pr_template(fix_result), }, ) async def _validate_fix(self, context: dict[str, Any]) -> AgentResult: """Validate that fix works correctly using ruff.""" fix = context.get("fix", {}) file_path = fix.get("file") if not file_path: return AgentResult( success=False, message="Fix data required for validation", ) validations = [] # ruff check try: import subprocess r = subprocess.run( ["ruff", "check", "--no-cache", str(_get_project_root() / file_path)], capture_output=True, text=True, timeout=30, ) ruff_passed = r.returncode == 0 validations.append({ "check": "ruff_lint", "result": "passed" if ruff_passed else "failed", "details": r.stdout.strip()[:500] if r.stdout else "No issues", }) except (FileNotFoundError, subprocess.TimeoutExpired, Exception) as e: validations.append({ "check": "ruff_lint", "result": "skipped", "details": str(e), }) # ruff format check try: r = subprocess.run( ["ruff", "format", "--check", "--no-cache", str(_get_project_root() / file_path)], capture_output=True, text=True, timeout=30, ) format_passed = r.returncode == 0 validations.append({ "check": "ruff_format", "result": "passed" if format_passed else "failed", "details": r.stdout.strip()[:500] if r.stdout else "Formatted correctly", }) except (FileNotFoundError, subprocess.TimeoutExpired, Exception) as e: validations.append({ "check": "ruff_format", "result": "skipped", "details": str(e), }) all_passed = all(v["result"] == "passed" for v in validations) return AgentResult( success=all_passed, message=f"Ruff validation {'пройдена' if all_passed else 'провалена'}", data={ "validations": validations, "fix_status": "ready" if all_passed else "needs_work", }, ) async def _suggest_fixes(self, context: dict[str, Any]) -> AgentResult: """Suggest multiple fix approaches.""" bug_data = context.get("bug_data", {}) error = bug_data.get("error_message", "Unknown") suggestions = [ { "approach": "minimal", "description": "Minimal change to fix specific issue", "risk": "low", "time_estimate": "5 minutes", }, { "approach": "refactored", "description": "Better solution with code improvement", "risk": "medium", "time_estimate": "20 minutes", }, { "approach": "comprehensive", "description": "Full fix with tests and documentation", "risk": "low", "time_estimate": "45 minutes", }, ] return AgentResult( success=True, message=f"Generated {len(suggestions)} fix suggestions", data={ "suggestions": suggestions, "recommended": "minimal" if bug_data.get("severity") == "low" else "comprehensive", }, ) def _identify_error_type(self, error: str) -> str: """Identify type of error.""" known_types = [ "AttributeError", "TypeError", "ValueError", "KeyError", "ImportError", "SyntaxError", "RuntimeError", "IndexError", "ZeroDivisionError", "FileNotFoundError", "ModuleNotFoundError", "StopIteration", ] for error_type in known_types: if error_type in error: return error_type error_types = { "AttributeError": r"'[^']+' object has no attribute", "TypeError": r"'[^']+' (object|instance)", "ValueError": r"invalid value", "KeyError": r"KeyError: '[^']+'", "ImportError": r"ImportError|Cannot import", } for error_type, pattern in error_types.items(): if re.search(pattern, error, re.IGNORECASE): return error_type return "UnknownError" def _identify_causes(self, error: str, stack_trace: str) -> list[str]: """Identify likely causes of the error.""" causes = [] if "NoneType" in error or "NoneType" in stack_trace: causes.append("Object is None when method is called") if "AttributeError" in error: causes.append("Missing attribute or wrong object type") if "KeyError" in error: causes.append("Missing dictionary key") if "IndexError" in error: causes.append("Index out of range") if not causes: causes.append("Requires deeper analysis of stack trace") return causes def _assess_severity(self, error: str) -> str: """Assess bug severity.""" critical_patterns = ["database", "authentication", "security", "corruption"] high_patterns = ["crash", "hang", "infinite loop"] if any(p in error.lower() for p in critical_patterns): return "critical" if any(p in error.lower() for p in high_patterns): return "high" return "medium" def _suggest_approach(self, error: str) -> str: """Suggest approach for fixing.""" if "AttributeError" in error: return "Add null check or use getattr with default" if "TypeError" in error: return "Add type validation or type casting" if "KeyError" in error: return "Use dict.get() with default or check key exists" if "ImportError" in error: return "Check import path and dependencies" return "Review stack trace for exact location" def _generate_fix_code(self, bug_data: dict, file_path: str) -> str: """Generate fix code snippet.""" return """```python # Suggested fix for {file_path} # Issue: {description} try: # Original code that failed result = object.method() except {error_type} as e: # Handle the error gracefully logger.warning(f"Error occurred: {{e}}") result = None # or appropriate fallback ``` Explanation: {explanation} ```""".format( file_path=file_path, description=bug_data.get("description", "Unknown issue"), error_type=self._identify_error_type(bug_data.get("error_message", "")), explanation=self._suggest_approach(bug_data.get("error_message", "")), ) def _generate_test(self, bug_data: dict) -> str: """Generate test case for the bug.""" return """```python def test_{test_name}(): \"\"\"Test for bug fix: {description}\"\"\" # Setup # ... # Execute result = function_under_test() # Assert assert result is not None assert result == expected_value ```""".format( test_name=bug_data.get("name", "fix").replace(" ", "_").lower(), description=bug_data.get("description", ""), ) def _generate_pr_template(self, fix: dict) -> str: """Generate PR description template.""" return """## Fix: {issue} ### Проблема {issue} ### Причина {cause} ### Решение {fix_description} ### Тесты - [ ] Добавлен тест для предотвращения - [ ] Существующие тесты проходят ### Логи Связанные логи из bug report """.format( issue=fix.get("issue", "Issue description"), cause="Identified root cause", fix_description=fix.get("proposed_fix", "Fix description"), ) async def get_metrics(self) -> dict[str, Any]: """Get FixAgent metrics.""" return { "agent_id": self.name, "version": self.version, "status": self.status.value, "fixes_generated": 0, "fixes_approved": 0, }