Files
project-lyra/cortex/reasoning/reflection.py
2025-11-29 05:14:32 -05:00

125 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Lyras 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()]}