sunee3 / core /auto_debug.py
sagarmythos
DevAI Studio HCBH v3.1 - clean deploy, no binaries
df6cd5e
Raw
History Blame Contribute Delete
8.99 kB
"""
core/auto_debug.py — parse → focused fix → re-test loop (max 3 attempts).
v4.6 additions
--------------
* Queries :mod:`core.patch_memory` for previously-failed patches that match
the current error signature; results are injected into the fix prompt as
"known-bad" guidance so the coder is nudged away from repeating them.
* On loop exhaustion, records the final failed patch back into patch memory
so future runs benefit from the same signal.
"""
from __future__ import annotations
import logging
import re
from datetime import datetime
from typing import Any, Dict, List, Optional
try:
from core.patch_memory import get_default_memory
except Exception: # pragma: no cover — memory is optional
get_default_memory = None # type: ignore[assignment]
class AutoDebugLoop:
MAX_AUTO_DEBUG_ATTEMPTS = 3
ERROR_PATTERNS = {
"python": r'File "(?P<file>[^"]+)", line (?P<line>\d+).*?\n(?P<error_type>\w+(?:Error|Exception)?):\s*(?P<message>.*)',
"node": r'(?P<error_type>\w+(?:Error|Exception)?):\s*(?P<message>.*)\s+at\s+(?P<file>[^:]+):(?P<line>\d+)',
"generic": r'(?P<error_type>\w+):\s*(?P<message>.*)',
}
# v4.6 — a lightweight fingerprint for lint-only failures so patch memory
# can distinguish "test failed" from "lint rejected the patch".
_LINT_MARKER = "[static-analysis] lint gate failed"
def __init__(self, memory=None) -> None:
# Injected for tests; falls back to the module-level singleton so
# existing call sites (``AutoDebugLoop()``) keep working unchanged.
self._memory = memory
# ------------------------------------------------------------------
# Memory access (lazy so tests can monkey-patch the module)
# ------------------------------------------------------------------
def _get_memory(self):
if self._memory is not None:
return self._memory
if get_default_memory is None:
return None
try:
return get_default_memory()
except Exception as exc: # pragma: no cover — never fatal
logging.warning("patch_memory unavailable: %s", exc)
return None
# ------------------------------------------------------------------
# Error parsing
# ------------------------------------------------------------------
def parse_error(self, log: str, language: str = "python") -> Dict[str, Any]:
pat = self.ERROR_PATTERNS.get(language, self.ERROR_PATTERNS["generic"])
m = re.search(pat, log or "", re.DOTALL)
if m:
g = m.groupdict()
line_s = g.get("line", "0")
diag = {
"error_type": g.get("error_type", "Unknown"),
"file_path": g.get("file", "unknown"),
"line_number": int(line_s) if str(line_s).isdigit() else 0,
"message": (g.get("message") or "").strip(),
"raw_log": log,
}
else:
diag = {
"error_type": "Unknown", "file_path": "unknown", "line_number": 0,
"message": (log or "")[:200], "raw_log": log,
}
if log and self._LINT_MARKER in log:
# Tag lint-only failures so patch memory can filter them later.
diag["error_type"] = diag.get("error_type") or "LintError"
diag["tags"] = ["lint"]
return diag
# ------------------------------------------------------------------
# Fix request (with patch-memory advisory injection)
# ------------------------------------------------------------------
def request_fix(self, diag: Dict[str, Any], coder, state: Dict[str, Any]
) -> Optional[str]:
advisory = ""
memory = self._get_memory()
if memory is not None:
try:
matches = memory.query_similar(diag)
if matches:
advisory = "\n\n" + memory.format_advisory(matches) + "\n\n"
state.setdefault("patch_memory_matches", []).extend(matches)
except Exception as exc: # pragma: no cover — memory is advisory
logging.warning("patch_memory query failed: %s", exc)
instruction = (
f"URGENT FIX: file `{diag['file_path']}` line {diag['line_number']}\n"
f"Error: {diag['error_type']}: {diag['message']}\n"
f"{advisory}"
f"Provide ONLY a minimal Unified Diff patch (or replacement code "
f"in a fenced block) fixing this exact error. "
f"Do NOT rewrite the entire file."
)
try:
new_state = coder.run(state.copy(), instruction=instruction)
return new_state.get("generated_code")
except Exception as exc:
logging.error("AutoDebug coder call failed: %s", exc)
return None
# ------------------------------------------------------------------
# Main loop
# ------------------------------------------------------------------
def run(self, error_log: str, coder, tester, state: Dict[str, Any],
language: str = "python") -> Dict[str, Any]:
timeline: List[Dict[str, Any]] = []
current = error_log
last_patch: Optional[str] = None
last_diag: Dict[str, Any] = {}
for attempt in range(1, self.MAX_AUTO_DEBUG_ATTEMPTS + 1):
diag = self.parse_error(current, language)
last_diag = diag
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "AutoDebug", "status": "running",
"message": f"attempt {attempt}/{self.MAX_AUTO_DEBUG_ATTEMPTS}: "
f"fixing {diag['error_type']}",
})
patch = self.request_fix(diag, coder, state)
if not patch:
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "AutoDebug", "status": "failed",
"message": f"attempt {attempt}: coder returned empty",
})
continue
last_patch = patch
state["generated_code"] = patch
state["file_path"] = diag["file_path"]
prev_flag = state.get("auto_debug_enabled", True)
state["auto_debug_enabled"] = False
try:
test_state = tester.run(state)
finally:
state["auto_debug_enabled"] = prev_flag
if test_state.get("test_passed"):
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "AutoDebug", "status": "success",
"message": f"attempt {attempt}: fixed",
})
return {
"success": True, "attempts": attempt,
"final_logs": test_state.get("test_logs", ""),
"timeline_entries": timeline,
}
current = test_state.get("test_logs", current)
# ------------------------------------------------------------------
# Exhausted → record final failed patch into memory for next time.
# ------------------------------------------------------------------
memory = self._get_memory()
if memory is not None and last_patch:
try:
stored = memory.record_failure(
diagnostic=last_diag,
patch_text=last_patch,
attempts=self.MAX_AUTO_DEBUG_ATTEMPTS,
tags=list(last_diag.get("tags", []) or []) + ["autodebug-exhausted"],
extra={
"language": language,
"command": state.get("command"),
},
)
state.setdefault("patch_memory_recorded", []).append(stored)
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "PatchMemory", "status": "OK",
"message": f"recorded failed patch {stored.get('id')}",
})
except Exception as exc: # pragma: no cover — never fatal
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "PatchMemory", "status": "FAIL",
"message": f"record failed: {exc}",
})
timeline.append({
"timestamp": datetime.now().strftime("%H:%M:%S"),
"agent": "AutoDebug", "status": "failed",
"message": f"all {self.MAX_AUTO_DEBUG_ATTEMPTS} attempts failed",
})
return {
"success": False,
"attempts": self.MAX_AUTO_DEBUG_ATTEMPTS,
"final_logs": current,
"timeline_entries": timeline,
}
__all__ = ["AutoDebugLoop"]