# reflection.py import json import os import re import logging from llm.llm_router import call_llm # Logger VERBOSE_DEBUG = os.getenv("VERBOSE_DEBUG", "false").lower() == "true" logger = logging.getLogger(__name__) if VERBOSE_DEBUG: logger.setLevel(logging.DEBUG) # Console handler console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter( '%(asctime)s [REFLECTION] %(levelname)s: %(message)s', datefmt='%H:%M:%S' )) logger.addHandler(console_handler) # File handler try: os.makedirs('/app/logs', exist_ok=True) file_handler = logging.FileHandler('/app/logs/cortex_verbose_debug.log', mode='a') file_handler.setFormatter(logging.Formatter( '%(asctime)s [REFLECTION] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' )) logger.addHandler(file_handler) logger.debug("VERBOSE_DEBUG mode enabled for reflection.py - logging to file") except Exception as e: logger.debug(f"VERBOSE_DEBUG mode enabled for reflection.py - file logging failed: {e}") async def reflect_notes(intake_summary: str, identity_block: dict | None) -> dict: """ Produce short internal reflection notes for Cortex. These are NOT shown to the user. """ # ----------------------------- # Build the prompt # ----------------------------- identity_text = "" if identity_block: identity_text = f"Identity:\n{identity_block}\n\n" prompt = ( f"{identity_text}" f"Recent summary:\n{intake_summary}\n\n" "You are Lyra's meta-awareness layer. Your job is to produce short, directive " "internal notes that guide Lyra’s reasoning engine. These notes are NEVER " "shown to the user.\n\n" "Rules for output:\n" "1. Return ONLY valid JSON.\n" "2. JSON must have exactly one key: \"notes\".\n" "3. \"notes\" must be a list of 3 to 6 short strings.\n" "4. Notes must be actionable (e.g., \"keep it concise\", \"maintain context\").\n" "5. No markdown, no apologies, no explanations.\n\n" "Return JSON:\n" "{ \"notes\": [\"...\"] }\n" ) # ----------------------------- # Module-specific backend choice # ----------------------------- reflection_backend = os.getenv("REFLECTION_LLM") cortex_backend = os.getenv("CORTEX_LLM", "PRIMARY").upper() # Reflection uses its own backend if set, otherwise cortex backend backend = (reflection_backend or cortex_backend).upper() # ----------------------------- # Call the selected LLM backend # ----------------------------- if VERBOSE_DEBUG: logger.debug(f"\n{'='*80}") logger.debug("[REFLECTION] Full prompt being sent to LLM:") logger.debug(f"{'='*80}") logger.debug(prompt) logger.debug(f"{'='*80}") logger.debug(f"Backend: {backend}") logger.debug(f"{'='*80}\n") raw = await call_llm(prompt, backend=backend) if VERBOSE_DEBUG: logger.debug(f"\n{'='*80}") logger.debug("[REFLECTION] LLM Response received:") logger.debug(f"{'='*80}") logger.debug(raw) logger.debug(f"{'='*80}\n") # ----------------------------- # Try direct JSON # ----------------------------- try: parsed = json.loads(raw.strip()) if isinstance(parsed, dict) and "notes" in parsed: if VERBOSE_DEBUG: logger.debug(f"[REFLECTION] Parsed {len(parsed['notes'])} notes from JSON") return parsed except: if VERBOSE_DEBUG: logger.debug("[REFLECTION] Direct JSON parsing failed, trying extraction...") # ----------------------------- # Try JSON extraction # ----------------------------- try: match = re.search(r"\{.*?\}", raw, re.S) if match: parsed = json.loads(match.group(0)) if isinstance(parsed, dict) and "notes" in parsed: return parsed except: pass # ----------------------------- # Fallback — treat raw text as a single note # ----------------------------- return {"notes": [raw.strip()]}