import json import re import sys from pathlib import Path from enum import Enum def get_base_dir() -> Path: if getattr(sys, "frozen", False): return Path(sys.executable).parent return Path(__file__).resolve().parent.parent BASE_DIR = get_base_dir() API_CONFIG_PATH = BASE_DIR / "config" / "api_keys.json" class ErrorDecision(Enum): RETRY = "retry" SKIP = "skip" REPLAN = "replan" ABORT = "abort" ERROR_ANALYST_PROMPT = """You are the error recovery module of MARK XXV AI assistant. A task step has failed. Analyze the error and decide what to do. DECISIONS: - retry : Transient error (network timeout, temporary file lock, race condition). The same step can succeed if tried again. - skip : This step is not critical and the task can succeed without it. - replan : The approach was wrong. A different tool or method should be tried. - abort : The task is fundamentally impossible or unsafe to continue. Also provide: - A brief explanation of WHY it failed (1 sentence) - A fix suggestion if decision is replan (what to try instead) - Max retries: how many times to retry if decision is retry (1 or 2) Return ONLY valid JSON: { "decision": "retry|skip|replan|abort", "reason": "why it failed", "fix_suggestion": "what to try instead (for replan)", "max_retries": 1, "user_message": "Short message to tell the user (max 15 words)" } """ def _get_api_key() -> str: with open(API_CONFIG_PATH, "r", encoding="utf-8") as f: return json.load(f)["gemini_api_key"] def analyze_error( step: dict, error: str, attempt: int = 1, max_attempts: int = 2 ) -> dict: """ Analyzes a failed step and returns a recovery decision. Args: step : The step dict that failed error : Error message/traceback attempt : Current attempt number max_attempts : How many times we've already tried Returns: { "decision": ErrorDecision, "reason": str, "fix_suggestion": str, "max_retries": int, "user_message": str } """ import google.generativeai as genai if attempt >= max_attempts: print(f"[ErrorHandler] ⚠️ Max attempts reached for step {step.get('step')} — forcing replan") return { "decision": ErrorDecision.REPLAN, "reason": f"Failed {attempt} times: {error[:100]}", "fix_suggestion": "Try a completely different approach or tool", "max_retries": 0, "user_message": "Trying a different approach, sir." } genai.configure(api_key=_get_api_key()) model = genai.GenerativeModel( model_name="gemini-2.5-flash-lite", system_instruction=ERROR_ANALYST_PROMPT ) prompt = f"""Failed step: Tool: {step.get('tool')} Description: {step.get('description')} Parameters: {json.dumps(step.get('parameters', {}), indent=2)} Critical: {step.get('critical', False)} Error: {error[:500]} Attempt number: {attempt}""" try: response = model.generate_content(prompt) text = response.text.strip() text = re.sub(r"```(?:json)?", "", text).strip().rstrip("`").strip() result = json.loads(text) decision_str = result.get("decision", "replan").lower() decision_map = { "retry": ErrorDecision.RETRY, "skip": ErrorDecision.SKIP, "replan": ErrorDecision.REPLAN, "abort": ErrorDecision.ABORT, } result["decision"] = decision_map.get(decision_str, ErrorDecision.REPLAN) if step.get("critical") and result["decision"] == ErrorDecision.SKIP: result["decision"] = ErrorDecision.REPLAN result["user_message"] = "This step is critical — finding alternative approach, sir." print(f"[ErrorHandler] Decision: {result['decision'].value} — {result.get('reason', '')}") return result except Exception as e: print(f"[ErrorHandler] ⚠️ Analysis failed: {e} — defaulting to replan") return { "decision": ErrorDecision.REPLAN, "reason": str(e), "fix_suggestion": "Try alternative approach", "max_retries": 1, "user_message": "Encountered an issue, adjusting approach, sir." } def generate_fix(step: dict, error: str, fix_suggestion: str) -> dict: """ When decision is REPLAN and a fix suggestion exists, generates a replacement step using generated_code as fallback. Returns a modified step dict. """ import google.generativeai as genai genai.configure(api_key=_get_api_key()) model = genai.GenerativeModel(model_name="gemini-2.0-flash") prompt = f"""A task step failed. Generate a replacement step. Original step: Tool: {step.get('tool')} Description: {step.get('description')} Parameters: {json.dumps(step.get('parameters', {}), indent=2)} Error: {error[:300]} Fix suggestion: {fix_suggestion} Write a Python script that accomplishes the same goal differently. Return ONLY the Python code, no explanation.""" try: response = model.generate_content(prompt) code = response.text.strip() code = re.sub(r"```(?:python)?", "", code).strip().rstrip("`").strip() return { "step": step.get("step"), "tool": "code_helper", "description": f"Auto-fix for: {step.get('description')}", "parameters": { "action": "run", "description": fix_suggestion, "code": code, "language": "python" }, "depends_on": step.get("depends_on", []), "critical": step.get("critical", False) } except Exception as e: print(f"[ErrorHandler] ⚠️ Fix generation failed: {e}") return { "step": step.get("step"), "tool": "generated_code", "description": f"Fallback for: {step.get('description')}", "parameters": {"description": step.get("description", "")}, "depends_on": step.get("depends_on", []), "critical": step.get("critical", False) }