125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
# 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()]}
|