Cortex debugging logs cleaned up
This commit is contained in:
132
.env.logging.example
Normal file
132
.env.logging.example
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# CORTEX LOGGING CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
# This file contains all logging-related environment variables for the
|
||||||
|
# Cortex reasoning pipeline. Copy this to your .env file and adjust as needed.
|
||||||
|
#
|
||||||
|
# Log Detail Levels:
|
||||||
|
# minimal - Only errors and critical events
|
||||||
|
# summary - Stage completion + errors (DEFAULT - RECOMMENDED FOR PRODUCTION)
|
||||||
|
# detailed - Include raw LLM outputs, RAG results, timing breakdowns
|
||||||
|
# verbose - Everything including intermediate states, full JSON dumps
|
||||||
|
#
|
||||||
|
# Quick Start:
|
||||||
|
# - For debugging weak links: LOG_DETAIL_LEVEL=detailed
|
||||||
|
# - For finding performance bottlenecks: LOG_DETAIL_LEVEL=detailed + VERBOSE_DEBUG=true
|
||||||
|
# - For production: LOG_DETAIL_LEVEL=summary
|
||||||
|
# - For silent mode: LOG_DETAIL_LEVEL=minimal
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Primary Logging Level
|
||||||
|
# -----------------------------
|
||||||
|
# Controls overall verbosity across all components
|
||||||
|
LOG_DETAIL_LEVEL=detailed
|
||||||
|
|
||||||
|
# Legacy verbose debug flag (kept for compatibility)
|
||||||
|
# When true, enables maximum logging including raw data dumps
|
||||||
|
VERBOSE_DEBUG=false
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# LLM Logging
|
||||||
|
# -----------------------------
|
||||||
|
# Enable raw LLM response logging (only works with detailed/verbose levels)
|
||||||
|
# Shows full JSON responses from each LLM backend call
|
||||||
|
# Set to "true" to see exact LLM outputs for debugging weak links
|
||||||
|
LOG_RAW_LLM_RESPONSES=true
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Context Logging
|
||||||
|
# -----------------------------
|
||||||
|
# Show full raw intake data (L1-L30 summaries) in logs
|
||||||
|
# WARNING: Very verbose, use only for deep debugging
|
||||||
|
LOG_RAW_CONTEXT_DATA=false
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Loop Detection & Protection
|
||||||
|
# -----------------------------
|
||||||
|
# Enable duplicate message detection to prevent processing loops
|
||||||
|
ENABLE_DUPLICATE_DETECTION=true
|
||||||
|
|
||||||
|
# Maximum number of messages to keep in session history (prevents unbounded growth)
|
||||||
|
# Older messages are trimmed automatically
|
||||||
|
MAX_MESSAGE_HISTORY=100
|
||||||
|
|
||||||
|
# Session TTL in hours - sessions inactive longer than this are auto-expired
|
||||||
|
SESSION_TTL_HOURS=24
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# NeoMem / RAG Logging
|
||||||
|
# -----------------------------
|
||||||
|
# Relevance score threshold for NeoMem results
|
||||||
|
RELEVANCE_THRESHOLD=0.4
|
||||||
|
|
||||||
|
# Enable NeoMem long-term memory retrieval
|
||||||
|
NEOMEM_ENABLED=false
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Autonomous Features
|
||||||
|
# -----------------------------
|
||||||
|
# Enable autonomous tool invocation (RAG, WEB, WEATHER, CODEBRAIN)
|
||||||
|
ENABLE_AUTONOMOUS_TOOLS=true
|
||||||
|
|
||||||
|
# Confidence threshold for autonomous tool invocation (0.0 - 1.0)
|
||||||
|
AUTONOMOUS_TOOL_CONFIDENCE_THRESHOLD=0.6
|
||||||
|
|
||||||
|
# Enable proactive monitoring and suggestions
|
||||||
|
ENABLE_PROACTIVE_MONITORING=true
|
||||||
|
|
||||||
|
# Minimum priority for proactive suggestions to be included (0.0 - 1.0)
|
||||||
|
PROACTIVE_SUGGESTION_MIN_PRIORITY=0.6
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# EXAMPLE LOGGING OUTPUT AT DIFFERENT LEVELS
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# LOG_DETAIL_LEVEL=summary (RECOMMENDED):
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ✅ [LLM] PRIMARY | 14:23:45.123 | Reply: Based on your question about...
|
||||||
|
# 📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 3 results
|
||||||
|
# 🧠 Monologue | question | Tone: curious
|
||||||
|
# ✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
# 📤 Output: 342 characters
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# LOG_DETAIL_LEVEL=detailed (FOR DEBUGGING):
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 🚀 PIPELINE START | Session: abc123 | 14:23:45.123
|
||||||
|
# 📝 User: What is the meaning of life?
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 🧠 LLM CALL | Backend: PRIMARY | 14:23:45.234
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 📝 Prompt: You are Lyra, a thoughtful AI assistant...
|
||||||
|
# 💬 Reply: Based on philosophical perspectives, the meaning...
|
||||||
|
# ╭─ RAW RESPONSE ────────────────────────────────────────────────────────────
|
||||||
|
# │ {
|
||||||
|
# │ "choices": [
|
||||||
|
# │ {
|
||||||
|
# │ "message": {
|
||||||
|
# │ "content": "Based on philosophical perspectives..."
|
||||||
|
# │ }
|
||||||
|
# │ }
|
||||||
|
# │ ]
|
||||||
|
# │ }
|
||||||
|
# ╰───────────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# ✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
# ⏱️ Stage Timings:
|
||||||
|
# context : 150ms ( 12.0%)
|
||||||
|
# identity : 10ms ( 0.8%)
|
||||||
|
# monologue : 200ms ( 16.0%)
|
||||||
|
# reasoning : 450ms ( 36.0%)
|
||||||
|
# refinement : 300ms ( 24.0%)
|
||||||
|
# persona : 140ms ( 11.2%)
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# LOG_DETAIL_LEVEL=verbose (MAXIMUM DEBUG):
|
||||||
|
# Same as detailed but includes:
|
||||||
|
# - Full 50+ line raw JSON dumps
|
||||||
|
# - Complete intake data structures
|
||||||
|
# - All intermediate processing states
|
||||||
|
# - Detailed traceback on errors
|
||||||
|
# ============================================================================
|
||||||
178
LOGGING_MIGRATION.md
Normal file
178
LOGGING_MIGRATION.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Logging System Migration Complete
|
||||||
|
|
||||||
|
## ✅ What Changed
|
||||||
|
|
||||||
|
The old `VERBOSE_DEBUG` logging system has been completely replaced with the new structured `LOG_DETAIL_LEVEL` system.
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
|
||||||
|
1. **[.env](.env)** - Removed `VERBOSE_DEBUG`, cleaned up duplicate `LOG_DETAIL_LEVEL` settings
|
||||||
|
2. **[cortex/.env](cortex/.env)** - Removed `VERBOSE_DEBUG` from cortex config
|
||||||
|
3. **[cortex/router.py](cortex/router.py)** - Replaced `VERBOSE_DEBUG` checks with `LOG_DETAIL_LEVEL`
|
||||||
|
4. **[cortex/context.py](cortex/context.py)** - Replaced `VERBOSE_DEBUG` with `LOG_DETAIL_LEVEL`, removed verbose file logging setup
|
||||||
|
|
||||||
|
## 🎯 New Logging Configuration
|
||||||
|
|
||||||
|
### Single Environment Variable
|
||||||
|
|
||||||
|
Set `LOG_DETAIL_LEVEL` in your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LOG_DETAIL_LEVEL=detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging Levels
|
||||||
|
|
||||||
|
| Level | Lines/Message | What You See |
|
||||||
|
|-------|---------------|--------------|
|
||||||
|
| **minimal** | 1-2 | Only errors and critical events |
|
||||||
|
| **summary** | 5-7 | Pipeline completion, errors, warnings (production mode) |
|
||||||
|
| **detailed** | 30-50 | LLM outputs, timing breakdowns, context (debugging mode) |
|
||||||
|
| **verbose** | 100+ | Everything including raw JSON dumps (deep debugging) |
|
||||||
|
|
||||||
|
## 📊 What You Get at Each Level
|
||||||
|
|
||||||
|
### Summary Mode (Production)
|
||||||
|
```
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 3 results
|
||||||
|
🧠 Monologue | question | Tone: curious
|
||||||
|
|
||||||
|
====================================================================================================
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
====================================================================================================
|
||||||
|
📤 Output: 342 characters
|
||||||
|
====================================================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Mode (Debugging - RECOMMENDED)
|
||||||
|
```
|
||||||
|
====================================================================================================
|
||||||
|
🚀 PIPELINE START | Session: abc123 | 14:23:45.123
|
||||||
|
====================================================================================================
|
||||||
|
📝 User: What is the meaning of life?
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
🧠 LLM CALL | Backend: PRIMARY | 14:23:45.234
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
📝 Prompt: You are Lyra, analyzing the user's question...
|
||||||
|
💬 Reply: Based on the context provided, here's my analysis...
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 3 results
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
[CONTEXT] Session abc123 | User: What is the meaning of life?
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Mode: default | Mood: neutral | Project: None
|
||||||
|
Tools: RAG, WEB, WEATHER, CODEBRAIN, POKERBRAIN
|
||||||
|
|
||||||
|
╭─ INTAKE SUMMARIES ────────────────────────────────────────────────
|
||||||
|
│ L1 : Last message discussed philosophy...
|
||||||
|
│ L5 : Recent 5 messages covered existential topics...
|
||||||
|
│ L10 : Past 10 messages showed curiosity pattern...
|
||||||
|
╰───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
╭─ RAG RESULTS (3) ──────────────────────────────────────────────
|
||||||
|
│ [1] 0.923 | Previous discussion about purpose...
|
||||||
|
│ [2] 0.891 | Note about existential philosophy...
|
||||||
|
│ [3] 0.867 | Memory of Viktor Frankl discussion...
|
||||||
|
╰───────────────────────────────────────────────────────────────────
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
🧠 Monologue | question | Tone: curious
|
||||||
|
|
||||||
|
====================================================================================================
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
====================================================================================================
|
||||||
|
⏱️ Stage Timings:
|
||||||
|
context : 150ms ( 12.0%)
|
||||||
|
identity : 10ms ( 0.8%)
|
||||||
|
monologue : 200ms ( 16.0%)
|
||||||
|
tools : 0ms ( 0.0%)
|
||||||
|
reflection : 50ms ( 4.0%)
|
||||||
|
reasoning : 450ms ( 36.0%) ← BOTTLENECK!
|
||||||
|
refinement : 300ms ( 24.0%)
|
||||||
|
persona : 140ms ( 11.2%)
|
||||||
|
learning : 50ms ( 4.0%)
|
||||||
|
📤 Output: 342 characters
|
||||||
|
====================================================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verbose Mode (Maximum Debug)
|
||||||
|
Same as detailed, plus:
|
||||||
|
- Full raw JSON responses from LLMs (50-line boxes)
|
||||||
|
- Complete intake data structures
|
||||||
|
- Stack traces on errors
|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
### For Finding Weak Links (Your Use Case)
|
||||||
|
```bash
|
||||||
|
# In .env:
|
||||||
|
LOG_DETAIL_LEVEL=detailed
|
||||||
|
|
||||||
|
# Restart services:
|
||||||
|
docker-compose restart cortex relay
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll now see:
|
||||||
|
- ✅ Which LLM backend is used
|
||||||
|
- ✅ What prompts are sent to each LLM
|
||||||
|
- ✅ What each LLM responds with
|
||||||
|
- ✅ Timing breakdown showing which stage is slow
|
||||||
|
- ✅ Context being used (RAG, intake summaries)
|
||||||
|
- ✅ Clean, hierarchical structure
|
||||||
|
|
||||||
|
### For Production
|
||||||
|
```bash
|
||||||
|
LOG_DETAIL_LEVEL=summary
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Deep Debugging
|
||||||
|
```bash
|
||||||
|
LOG_DETAIL_LEVEL=verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Finding Performance Bottlenecks
|
||||||
|
|
||||||
|
With `detailed` mode, look for:
|
||||||
|
|
||||||
|
1. **Slow stages in timing breakdown:**
|
||||||
|
```
|
||||||
|
reasoning : 3450ms ( 76.0%) ← THIS IS YOUR BOTTLENECK!
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Backend failures:**
|
||||||
|
```
|
||||||
|
⚠️ [LLM] PRIMARY failed | 14:23:45.234 | Connection timeout
|
||||||
|
✅ [LLM] SECONDARY | Reply: Based on... ← Fell back to secondary
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Loop detection:**
|
||||||
|
```
|
||||||
|
⚠️ DUPLICATE MESSAGE DETECTED | Session: abc123
|
||||||
|
🔁 LOOP DETECTED - Returning cached context
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Removed Features
|
||||||
|
|
||||||
|
The following old logging features have been removed:
|
||||||
|
|
||||||
|
- ❌ `VERBOSE_DEBUG` environment variable (replaced with `LOG_DETAIL_LEVEL`)
|
||||||
|
- ❌ File logging to `/app/logs/cortex_verbose_debug.log` (use `docker logs` instead)
|
||||||
|
- ❌ Separate verbose handlers in Python logging
|
||||||
|
- ❌ Per-module verbose flags
|
||||||
|
|
||||||
|
## ✨ New Features
|
||||||
|
|
||||||
|
- ✅ Single unified logging configuration
|
||||||
|
- ✅ Hierarchical, scannable output
|
||||||
|
- ✅ Collapsible data sections (boxes)
|
||||||
|
- ✅ Stage timing always shown in detailed mode
|
||||||
|
- ✅ Performance profiling built-in
|
||||||
|
- ✅ Loop detection and warnings
|
||||||
|
- ✅ Clean error formatting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**The logging is now clean, concise, and gives you exactly what you need to find weak links!** 🎯
|
||||||
176
LOGGING_QUICK_REF.md
Normal file
176
LOGGING_QUICK_REF.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Cortex Logging Quick Reference
|
||||||
|
|
||||||
|
## 🎯 TL;DR
|
||||||
|
|
||||||
|
**Finding weak links in the LLM chain?**
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=detailed
|
||||||
|
export VERBOSE_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production use?**
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=summary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Log Levels Comparison
|
||||||
|
|
||||||
|
| Level | Output Lines/Message | Use Case | Raw LLM Output? |
|
||||||
|
|-------|---------------------|----------|-----------------|
|
||||||
|
| **minimal** | 1-2 | Silent production | ❌ No |
|
||||||
|
| **summary** | 5-7 | Production (DEFAULT) | ❌ No |
|
||||||
|
| **detailed** | 30-50 | Debugging, finding bottlenecks | ✅ Parsed only |
|
||||||
|
| **verbose** | 100+ | Deep debugging, seeing raw data | ✅ Full JSON |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Common Debugging Tasks
|
||||||
|
|
||||||
|
### See Raw LLM Outputs
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=verbose
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
╭─ RAW RESPONSE ────────────────────────────────────
|
||||||
|
│ { "choices": [ { "message": { "content": "..." } } ] }
|
||||||
|
╰───────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find Performance Bottlenecks
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=detailed
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
⏱️ Stage Timings:
|
||||||
|
reasoning : 3450ms ( 76.0%) ← SLOW!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Which RAG Memories Are Used
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=detailed
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
╭─ RAG RESULTS (5) ──────────────────────────────
|
||||||
|
│ [1] 0.923 | Memory content...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detect Loops
|
||||||
|
```bash
|
||||||
|
export ENABLE_DUPLICATE_DETECTION=true # (default)
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
⚠️ DUPLICATE MESSAGE DETECTED
|
||||||
|
🔁 LOOP DETECTED - Returning cached context
|
||||||
|
```
|
||||||
|
|
||||||
|
### See All Backend Failures
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=summary # or higher
|
||||||
|
```
|
||||||
|
Look for:
|
||||||
|
```
|
||||||
|
⚠️ [LLM] PRIMARY failed | Connection timeout
|
||||||
|
⚠️ [LLM] SECONDARY failed | Model not found
|
||||||
|
✅ [LLM] CLOUD | Reply: Based on...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Environment Variables Cheat Sheet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verbosity Control
|
||||||
|
LOG_DETAIL_LEVEL=detailed # minimal | summary | detailed | verbose
|
||||||
|
VERBOSE_DEBUG=false # true = maximum verbosity (legacy)
|
||||||
|
|
||||||
|
# Raw Data Visibility
|
||||||
|
LOG_RAW_CONTEXT_DATA=false # Show full intake L1-L30 dumps
|
||||||
|
|
||||||
|
# Loop Protection
|
||||||
|
ENABLE_DUPLICATE_DETECTION=true # Detect duplicate messages
|
||||||
|
MAX_MESSAGE_HISTORY=100 # Trim history after N messages
|
||||||
|
SESSION_TTL_HOURS=24 # Expire sessions after N hours
|
||||||
|
|
||||||
|
# Features
|
||||||
|
NEOMEM_ENABLED=false # Enable long-term memory
|
||||||
|
ENABLE_AUTONOMOUS_TOOLS=true # Enable tool invocation
|
||||||
|
ENABLE_PROACTIVE_MONITORING=true # Enable suggestions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Sample Output
|
||||||
|
|
||||||
|
### Summary Mode (Default - Production)
|
||||||
|
```
|
||||||
|
✅ [LLM] PRIMARY | 14:23:45.123 | Reply: Based on your question...
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 5 results
|
||||||
|
🧠 Monologue | question | Tone: curious
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
📤 Output: 342 characters
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Mode (Debugging)
|
||||||
|
```
|
||||||
|
════════════════════════════════════════════════════════════════════════════
|
||||||
|
🚀 PIPELINE START | Session: abc123 | 14:23:45.123
|
||||||
|
════════════════════════════════════════════════════════════════════════════
|
||||||
|
📝 User: What is the meaning of life?
|
||||||
|
────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────────
|
||||||
|
🧠 LLM CALL | Backend: PRIMARY | 14:23:45.234
|
||||||
|
────────────────────────────────────────────────────────────────────────────
|
||||||
|
📝 Prompt: You are Lyra, a thoughtful AI assistant...
|
||||||
|
💬 Reply: Based on philosophical perspectives...
|
||||||
|
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 5 results
|
||||||
|
╭─ RAG RESULTS (5) ──────────────────────────────
|
||||||
|
│ [1] 0.923 | Previous philosophy discussion...
|
||||||
|
│ [2] 0.891 | Existential note...
|
||||||
|
╰────────────────────────────────────────────────
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════════════
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
════════════════════════════════════════════════════════════════════════════
|
||||||
|
⏱️ Stage Timings:
|
||||||
|
context : 150ms ( 12.0%)
|
||||||
|
reasoning : 450ms ( 36.0%) ← Largest component
|
||||||
|
persona : 140ms ( 11.2%)
|
||||||
|
📤 Output: 342 characters
|
||||||
|
════════════════════════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Quick Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Check | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| **Logs too verbose** | Current level | Set `LOG_DETAIL_LEVEL=summary` |
|
||||||
|
| **Can't see LLM outputs** | Current level | Set `LOG_DETAIL_LEVEL=detailed` or `verbose` |
|
||||||
|
| **Repeating operations** | Loop warnings | Check for `🔁 LOOP DETECTED` messages |
|
||||||
|
| **Slow responses** | Stage timings | Look for stages >1000ms in detailed mode |
|
||||||
|
| **Missing RAG data** | NEOMEM_ENABLED | Set `NEOMEM_ENABLED=true` |
|
||||||
|
| **Out of memory** | Message history | Lower `MAX_MESSAGE_HISTORY` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Key Files
|
||||||
|
|
||||||
|
- **[.env.logging.example](.env.logging.example)** - Full configuration guide
|
||||||
|
- **[LOGGING_REFACTOR_SUMMARY.md](LOGGING_REFACTOR_SUMMARY.md)** - Detailed explanation
|
||||||
|
- **[cortex/utils/logging_utils.py](cortex/utils/logging_utils.py)** - Logging utilities
|
||||||
|
- **[cortex/context.py](cortex/context.py)** - Context + loop protection
|
||||||
|
- **[cortex/router.py](cortex/router.py)** - Pipeline stages
|
||||||
|
- **[core/relay/lib/llm.js](core/relay/lib/llm.js)** - LLM backend logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need more detail? See [LOGGING_REFACTOR_SUMMARY.md](LOGGING_REFACTOR_SUMMARY.md)**
|
||||||
352
LOGGING_REFACTOR_SUMMARY.md
Normal file
352
LOGGING_REFACTOR_SUMMARY.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Cortex Logging Refactor Summary
|
||||||
|
|
||||||
|
## 🎯 Problem Statement
|
||||||
|
|
||||||
|
The cortex chat loop had severe logging issues that made debugging impossible:
|
||||||
|
|
||||||
|
1. **Massive verbosity**: 100+ log lines per chat message
|
||||||
|
2. **Raw LLM dumps**: Full JSON responses pretty-printed on every call (1000s of lines)
|
||||||
|
3. **Repeated data**: NeoMem results logged 71 times individually
|
||||||
|
4. **No structure**: Scattered emoji logs with no hierarchy
|
||||||
|
5. **Impossible to debug**: Couldn't tell if loops were happening or just verbose logging
|
||||||
|
6. **No loop protection**: Unbounded message history growth, no session cleanup, no duplicate detection
|
||||||
|
|
||||||
|
## ✅ What Was Fixed
|
||||||
|
|
||||||
|
### 1. **Structured Hierarchical Logging**
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```
|
||||||
|
🔍 RAW LLM RESPONSE: {
|
||||||
|
"id": "chatcmpl-123",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1234567890,
|
||||||
|
"model": "gpt-4",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Here is a very long response that goes on for hundreds of lines..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 123,
|
||||||
|
"completion_tokens": 456,
|
||||||
|
"total_tokens": 579
|
||||||
|
}
|
||||||
|
}
|
||||||
|
🧠 Trying backend: PRIMARY (http://localhost:8000)
|
||||||
|
✅ Success via PRIMARY
|
||||||
|
[STAGE 0] Collecting unified context...
|
||||||
|
[STAGE 0] Context collected - 5 RAG results
|
||||||
|
[COLLECT_CONTEXT] Intake data retrieved:
|
||||||
|
{
|
||||||
|
"L1": [...],
|
||||||
|
"L5": [...],
|
||||||
|
"L10": {...},
|
||||||
|
"L20": {...},
|
||||||
|
"L30": {...}
|
||||||
|
}
|
||||||
|
[COLLECT_CONTEXT] NeoMem search returned 71 results
|
||||||
|
[1] Score: 0.923 - Memory content here...
|
||||||
|
[2] Score: 0.891 - More memory content...
|
||||||
|
[3] Score: 0.867 - Even more content...
|
||||||
|
... (68 more lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (summary mode - DEFAULT):**
|
||||||
|
```
|
||||||
|
✅ [LLM] PRIMARY | 14:23:45.123 | Reply: Based on your question about...
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 5 results
|
||||||
|
🧠 Monologue | question | Tone: curious
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
📤 Output: 342 characters
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (detailed mode - for debugging):**
|
||||||
|
```
|
||||||
|
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
🚀 PIPELINE START | Session: abc123 | 14:23:45.123
|
||||||
|
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
📝 User: What is the meaning of life?
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
🧠 LLM CALL | Backend: PRIMARY | 14:23:45.234
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
📝 Prompt: You are Lyra, a thoughtful AI assistant...
|
||||||
|
💬 Reply: Based on philosophical perspectives, the meaning...
|
||||||
|
|
||||||
|
📊 Context | Session: abc123 | Messages: 42 | Last: 5.2min | RAG: 5 results
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
[CONTEXT] Session abc123 | User: What is the meaning of life?
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Mode: default | Mood: neutral | Project: None
|
||||||
|
Tools: RAG, WEB, WEATHER, CODEBRAIN, POKERBRAIN
|
||||||
|
|
||||||
|
╭─ INTAKE SUMMARIES ────────────────────────────────────────────────
|
||||||
|
│ L1 : Last message discussed philosophy...
|
||||||
|
│ L5 : Recent 5 messages covered existential topics...
|
||||||
|
│ L10 : Past 10 messages showed curiosity pattern...
|
||||||
|
│ L20 : Session focused on deep questions...
|
||||||
|
│ L30 : Long-term trend shows philosophical interest...
|
||||||
|
╰───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
╭─ RAG RESULTS (5) ──────────────────────────────────────────────
|
||||||
|
│ [1] 0.923 | Previous discussion about purpose and meaning...
|
||||||
|
│ [2] 0.891 | Note about existential philosophy...
|
||||||
|
│ [3] 0.867 | Memory of Viktor Frankl discussion...
|
||||||
|
│ [4] 0.834 | Reference to stoic philosophy...
|
||||||
|
│ [5] 0.801 | Buddhism and the middle path...
|
||||||
|
╰───────────────────────────────────────────────────────────────────
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
⏱️ Stage Timings:
|
||||||
|
context : 150ms ( 12.0%)
|
||||||
|
identity : 10ms ( 0.8%)
|
||||||
|
monologue : 200ms ( 16.0%)
|
||||||
|
tools : 0ms ( 0.0%)
|
||||||
|
reflection : 50ms ( 4.0%)
|
||||||
|
reasoning : 450ms ( 36.0%)
|
||||||
|
refinement : 300ms ( 24.0%)
|
||||||
|
persona : 140ms ( 11.2%)
|
||||||
|
📤 Output: 342 characters
|
||||||
|
════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Configurable Verbosity Levels**
|
||||||
|
|
||||||
|
Set via `LOG_DETAIL_LEVEL` environment variable:
|
||||||
|
|
||||||
|
- **`minimal`**: Only errors and critical events
|
||||||
|
- **`summary`**: Stage completion + errors (DEFAULT - recommended for production)
|
||||||
|
- **`detailed`**: Include raw LLM outputs, RAG results, timing breakdowns (for debugging)
|
||||||
|
- **`verbose`**: Everything including full JSON dumps (for deep debugging)
|
||||||
|
|
||||||
|
### 3. **Raw LLM Output Visibility** ✅
|
||||||
|
|
||||||
|
**You can now see raw LLM outputs clearly!**
|
||||||
|
|
||||||
|
In `detailed` or `verbose` mode, LLM calls show:
|
||||||
|
- Backend used
|
||||||
|
- Prompt preview
|
||||||
|
- Parsed reply
|
||||||
|
- **Raw JSON response in collapsible format** (verbose only)
|
||||||
|
|
||||||
|
```
|
||||||
|
╭─ RAW RESPONSE ────────────────────────────────────────────────────────────────────────────
|
||||||
|
│ {
|
||||||
|
│ "id": "chatcmpl-123",
|
||||||
|
│ "object": "chat.completion",
|
||||||
|
│ "model": "gpt-4",
|
||||||
|
│ "choices": [
|
||||||
|
│ {
|
||||||
|
│ "message": {
|
||||||
|
│ "content": "Full response here..."
|
||||||
|
│ }
|
||||||
|
│ }
|
||||||
|
│ ]
|
||||||
|
│ }
|
||||||
|
╰───────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Loop Detection & Protection** ✅
|
||||||
|
|
||||||
|
**New safety features:**
|
||||||
|
|
||||||
|
- **Duplicate message detection**: Prevents processing the same message twice
|
||||||
|
- **Message history trimming**: Auto-trims to last 100 messages (configurable via `MAX_MESSAGE_HISTORY`)
|
||||||
|
- **Session TTL**: Auto-expires inactive sessions after 24 hours (configurable via `SESSION_TTL_HOURS`)
|
||||||
|
- **Hash-based detection**: Uses MD5 hash to detect exact duplicate messages
|
||||||
|
|
||||||
|
**Example warning when loop detected:**
|
||||||
|
```
|
||||||
|
⚠️ DUPLICATE MESSAGE DETECTED | Session: abc123 | Message: What is the meaning of life?
|
||||||
|
🔁 LOOP DETECTED - Returning cached context to prevent processing duplicate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Performance Timing** ✅
|
||||||
|
|
||||||
|
In `detailed` mode, see exactly where time is spent:
|
||||||
|
|
||||||
|
```
|
||||||
|
⏱️ Stage Timings:
|
||||||
|
context : 150ms ( 12.0%) ← Context collection
|
||||||
|
identity : 10ms ( 0.8%) ← Identity loading
|
||||||
|
monologue : 200ms ( 16.0%) ← Inner monologue
|
||||||
|
tools : 0ms ( 0.0%) ← Autonomous tools
|
||||||
|
reflection : 50ms ( 4.0%) ← Reflection notes
|
||||||
|
reasoning : 450ms ( 36.0%) ← Main reasoning (BOTTLENECK)
|
||||||
|
refinement : 300ms ( 24.0%) ← Answer refinement
|
||||||
|
persona : 140ms ( 11.2%) ← Persona layer
|
||||||
|
```
|
||||||
|
|
||||||
|
**This helps you identify weak links in the chain!**
|
||||||
|
|
||||||
|
## 📁 Files Modified
|
||||||
|
|
||||||
|
### Core Changes
|
||||||
|
|
||||||
|
1. **[llm.js](core/relay/lib/llm.js)**
|
||||||
|
- Removed massive JSON dump on line 53
|
||||||
|
- Added structured logging with 4 verbosity levels
|
||||||
|
- Shows raw responses only in verbose mode (collapsible format)
|
||||||
|
- Tracks failed backends and shows summary on total failure
|
||||||
|
|
||||||
|
2. **[context.py](cortex/context.py)**
|
||||||
|
- Condensed 71-line NeoMem loop to 5-line summary
|
||||||
|
- Removed repeated intake data dumps
|
||||||
|
- Added structured hierarchical logging with boxes
|
||||||
|
- Added duplicate message detection
|
||||||
|
- Added message history trimming
|
||||||
|
- Added session TTL and cleanup
|
||||||
|
|
||||||
|
3. **[router.py](cortex/router.py)**
|
||||||
|
- Replaced 15+ stage logs with unified pipeline summary
|
||||||
|
- Added stage timing collection
|
||||||
|
- Shows performance breakdown in detailed mode
|
||||||
|
- Clean start/end markers with total duration
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
|
||||||
|
4. **[utils/logging_utils.py](cortex/utils/logging_utils.py)** (NEW)
|
||||||
|
- Reusable structured logging utilities
|
||||||
|
- `PipelineLogger` class for hierarchical logging
|
||||||
|
- Collapsible data sections
|
||||||
|
- Stage timing tracking
|
||||||
|
- Future-ready for expansion
|
||||||
|
|
||||||
|
5. **[.env.logging.example](.env.logging.example)** (NEW)
|
||||||
|
- Complete logging configuration guide
|
||||||
|
- Shows example output at each verbosity level
|
||||||
|
- Documents all environment variables
|
||||||
|
- Production-ready defaults
|
||||||
|
|
||||||
|
6. **[LOGGING_REFACTOR_SUMMARY.md](LOGGING_REFACTOR_SUMMARY.md)** (THIS FILE)
|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
### For Finding Weak Links (Your Use Case)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set in your .env or export:
|
||||||
|
export LOG_DETAIL_LEVEL=detailed
|
||||||
|
export VERBOSE_DEBUG=false # or true for even more detail
|
||||||
|
|
||||||
|
# Now run your chat - you'll see:
|
||||||
|
# 1. Which LLM backend is used
|
||||||
|
# 2. Raw LLM outputs (in verbose mode)
|
||||||
|
# 3. Exact timing per stage
|
||||||
|
# 4. Which stage is taking longest
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=summary
|
||||||
|
|
||||||
|
# Minimal, clean logs:
|
||||||
|
# ✅ [LLM] PRIMARY | 14:23:45.123 | Reply: Based on your question...
|
||||||
|
# ✨ PIPELINE COMPLETE | Session: abc123 | Total: 1250ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Deep Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LOG_DETAIL_LEVEL=verbose
|
||||||
|
export LOG_RAW_CONTEXT_DATA=true
|
||||||
|
|
||||||
|
# Shows EVERYTHING including full JSON dumps
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Finding Weak Links - Quick Guide
|
||||||
|
|
||||||
|
**Problem: "Which LLM stage is failing or producing bad output?"**
|
||||||
|
|
||||||
|
1. Set `LOG_DETAIL_LEVEL=detailed`
|
||||||
|
2. Run a test conversation
|
||||||
|
3. Look for timing anomalies:
|
||||||
|
```
|
||||||
|
reasoning : 3450ms ( 76.0%) ← BOTTLENECK!
|
||||||
|
```
|
||||||
|
4. Look for errors:
|
||||||
|
```
|
||||||
|
⚠️ Reflection failed: Connection timeout
|
||||||
|
```
|
||||||
|
5. Check raw LLM outputs (set `VERBOSE_DEBUG=true`):
|
||||||
|
```
|
||||||
|
╭─ RAW RESPONSE ────────────────────────────────────
|
||||||
|
│ {
|
||||||
|
│ "choices": [
|
||||||
|
│ { "message": { "content": "..." } }
|
||||||
|
│ ]
|
||||||
|
│ }
|
||||||
|
╰───────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem: "Is the loop repeating operations?"**
|
||||||
|
|
||||||
|
1. Enable duplicate detection (on by default)
|
||||||
|
2. Look for loop warnings:
|
||||||
|
```
|
||||||
|
⚠️ DUPLICATE MESSAGE DETECTED | Session: abc123
|
||||||
|
🔁 LOOP DETECTED - Returning cached context
|
||||||
|
```
|
||||||
|
3. Check stage timings - repeated stages will show up as duplicates
|
||||||
|
|
||||||
|
**Problem: "Which RAG memories are being used?"**
|
||||||
|
|
||||||
|
1. Set `LOG_DETAIL_LEVEL=detailed`
|
||||||
|
2. Look for RAG results box:
|
||||||
|
```
|
||||||
|
╭─ RAG RESULTS (5) ──────────────────────────────
|
||||||
|
│ [1] 0.923 | Previous discussion about X...
|
||||||
|
│ [2] 0.891 | Note about Y...
|
||||||
|
╰────────────────────────────────────────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Environment Variables Reference
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `LOG_DETAIL_LEVEL` | `summary` | Verbosity: minimal/summary/detailed/verbose |
|
||||||
|
| `VERBOSE_DEBUG` | `false` | Legacy flag for maximum verbosity |
|
||||||
|
| `LOG_RAW_CONTEXT_DATA` | `false` | Show full intake data dumps |
|
||||||
|
| `ENABLE_DUPLICATE_DETECTION` | `true` | Detect and prevent duplicate messages |
|
||||||
|
| `MAX_MESSAGE_HISTORY` | `100` | Max messages to keep per session |
|
||||||
|
| `SESSION_TTL_HOURS` | `24` | Auto-expire sessions after N hours |
|
||||||
|
|
||||||
|
## 🎉 Results
|
||||||
|
|
||||||
|
**Before:** 1000+ lines of logs per chat message, unreadable, couldn't identify issues
|
||||||
|
|
||||||
|
**After (summary mode):** 5 lines of structured logs, clear and actionable
|
||||||
|
|
||||||
|
**After (detailed mode):** ~50 lines with full visibility into each stage, timing, and raw outputs
|
||||||
|
|
||||||
|
**Loop protection:** Automatic detection and prevention of duplicate processing
|
||||||
|
|
||||||
|
**You can now:**
|
||||||
|
✅ See raw LLM outputs clearly (in detailed/verbose mode)
|
||||||
|
✅ Identify performance bottlenecks (stage timings)
|
||||||
|
✅ Detect loops and duplicates (automatic)
|
||||||
|
✅ Find failing stages (error markers)
|
||||||
|
✅ Scan logs quickly (hierarchical structure)
|
||||||
|
✅ Debug production issues (adjustable verbosity)
|
||||||
|
|
||||||
|
## 🔧 Next Steps (Optional Improvements)
|
||||||
|
|
||||||
|
1. **Structured JSON logging**: Output as JSON for log aggregation tools
|
||||||
|
2. **Log rotation**: Implement file rotation for verbose logs
|
||||||
|
3. **Metrics export**: Export stage timings to Prometheus/Grafana
|
||||||
|
4. **Error categorization**: Tag errors by type (network, timeout, parsing, etc.)
|
||||||
|
5. **Performance alerts**: Auto-alert when stages exceed thresholds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy debugging! You can now see what's actually happening in the cortex loop.** 🎯
|
||||||
@@ -38,6 +38,8 @@ async function tryBackend(backend, messages) {
|
|||||||
|
|
||||||
// 🧩 Normalize replies
|
// 🧩 Normalize replies
|
||||||
let reply = "";
|
let reply = "";
|
||||||
|
let parsedData = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isOllama) {
|
if (isOllama) {
|
||||||
// Ollama sometimes returns NDJSON lines; merge them
|
// Ollama sometimes returns NDJSON lines; merge them
|
||||||
@@ -49,21 +51,75 @@ async function tryBackend(backend, messages) {
|
|||||||
.join("");
|
.join("");
|
||||||
reply = merged.trim();
|
reply = merged.trim();
|
||||||
} else {
|
} else {
|
||||||
const data = JSON.parse(raw);
|
parsedData = JSON.parse(raw);
|
||||||
console.log("🔍 RAW LLM RESPONSE:", JSON.stringify(data, null, 2));
|
|
||||||
reply =
|
reply =
|
||||||
data?.choices?.[0]?.text?.trim() ||
|
parsedData?.choices?.[0]?.text?.trim() ||
|
||||||
data?.choices?.[0]?.message?.content?.trim() ||
|
parsedData?.choices?.[0]?.message?.content?.trim() ||
|
||||||
data?.message?.content?.trim() ||
|
parsedData?.message?.content?.trim() ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reply = `[parse error: ${err.message}]`;
|
reply = `[parse error: ${err.message}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { reply, raw, backend: backend.key };
|
return { reply, raw, parsedData, backend: backend.key };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Structured logging helper
|
||||||
|
// ------------------------------------
|
||||||
|
const LOG_DETAIL = process.env.LOG_DETAIL_LEVEL || "summary"; // minimal | summary | detailed | verbose
|
||||||
|
|
||||||
|
function logLLMCall(backend, messages, result, error = null) {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Always log errors
|
||||||
|
console.warn(`⚠️ [LLM] ${backend.key.toUpperCase()} failed | ${timestamp} | ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - log based on detail level
|
||||||
|
if (LOG_DETAIL === "minimal") {
|
||||||
|
return; // Don't log successful calls in minimal mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOG_DETAIL === "summary") {
|
||||||
|
console.log(`✅ [LLM] ${backend.key.toUpperCase()} | ${timestamp} | Reply: ${result.reply.substring(0, 80)}...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed or verbose
|
||||||
|
console.log(`\n${'─'.repeat(100)}`);
|
||||||
|
console.log(`🧠 LLM CALL | Backend: ${backend.key.toUpperCase()} | ${timestamp}`);
|
||||||
|
console.log(`${'─'.repeat(100)}`);
|
||||||
|
|
||||||
|
// Show prompt preview
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
const promptPreview = (lastMsg?.content || '').substring(0, 150);
|
||||||
|
console.log(`📝 Prompt: ${promptPreview}...`);
|
||||||
|
|
||||||
|
// Show parsed reply
|
||||||
|
console.log(`💬 Reply: ${result.reply.substring(0, 200)}...`);
|
||||||
|
|
||||||
|
// Show raw response only in verbose mode
|
||||||
|
if (LOG_DETAIL === "verbose" && result.parsedData) {
|
||||||
|
console.log(`\n╭─ RAW RESPONSE ────────────────────────────────────────────────────────────────────────────`);
|
||||||
|
const jsonStr = JSON.stringify(result.parsedData, null, 2);
|
||||||
|
const lines = jsonStr.split('\n');
|
||||||
|
const maxLines = 50;
|
||||||
|
|
||||||
|
lines.slice(0, maxLines).forEach(line => {
|
||||||
|
console.log(`│ ${line}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lines.length > maxLines) {
|
||||||
|
console.log(`│ ... (${lines.length - maxLines} more lines - check raw field for full response)`);
|
||||||
|
}
|
||||||
|
console.log(`╰${'─'.repeat(95)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${'─'.repeat(100)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@@ -77,17 +133,29 @@ export async function callSpeechLLM(messages) {
|
|||||||
{ key: "fallback", type: "llamacpp", url: process.env.LLM_FALLBACK_URL, model: process.env.LLM_FALLBACK_MODEL },
|
{ key: "fallback", type: "llamacpp", url: process.env.LLM_FALLBACK_URL, model: process.env.LLM_FALLBACK_MODEL },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const failedBackends = [];
|
||||||
|
|
||||||
for (const b of backends) {
|
for (const b of backends) {
|
||||||
if (!b.url || !b.model) continue;
|
if (!b.url || !b.model) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`🧠 Trying backend: ${b.key.toUpperCase()} (${b.url})`);
|
|
||||||
const out = await tryBackend(b, messages);
|
const out = await tryBackend(b, messages);
|
||||||
console.log(`✅ Success via ${b.key.toUpperCase()}`);
|
logLLMCall(b, messages, out);
|
||||||
return out;
|
return out;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`⚠️ ${b.key.toUpperCase()} failed: ${err.message}`);
|
logLLMCall(b, messages, null, err);
|
||||||
|
failedBackends.push({ backend: b.key, error: err.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All backends failed - log summary
|
||||||
|
console.error(`\n${'='.repeat(100)}`);
|
||||||
|
console.error(`🔴 ALL LLM BACKENDS FAILED`);
|
||||||
|
console.error(`${'='.repeat(100)}`);
|
||||||
|
failedBackends.forEach(({ backend, error }) => {
|
||||||
|
console.error(` ${backend.toUpperCase()}: ${error}`);
|
||||||
|
});
|
||||||
|
console.error(`${'='.repeat(100)}\n`);
|
||||||
|
|
||||||
throw new Error("all_backends_failed");
|
throw new Error("all_backends_failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ from neomem_client import NeoMemClient
|
|||||||
NEOMEM_API = os.getenv("NEOMEM_API", "http://neomem-api:8000")
|
NEOMEM_API = os.getenv("NEOMEM_API", "http://neomem-api:8000")
|
||||||
NEOMEM_ENABLED = os.getenv("NEOMEM_ENABLED", "false").lower() == "true"
|
NEOMEM_ENABLED = os.getenv("NEOMEM_ENABLED", "false").lower() == "true"
|
||||||
RELEVANCE_THRESHOLD = float(os.getenv("RELEVANCE_THRESHOLD", "0.4"))
|
RELEVANCE_THRESHOLD = float(os.getenv("RELEVANCE_THRESHOLD", "0.4"))
|
||||||
VERBOSE_DEBUG = os.getenv("VERBOSE_DEBUG", "false").lower() == "true"
|
LOG_DETAIL_LEVEL = os.getenv("LOG_DETAIL_LEVEL", "summary").lower()
|
||||||
|
|
||||||
|
# Loop detection settings
|
||||||
|
MAX_MESSAGE_HISTORY = int(os.getenv("MAX_MESSAGE_HISTORY", "100")) # Prevent unbounded growth
|
||||||
|
SESSION_TTL_HOURS = int(os.getenv("SESSION_TTL_HOURS", "24")) # Auto-expire old sessions
|
||||||
|
ENABLE_DUPLICATE_DETECTION = os.getenv("ENABLE_DUPLICATE_DETECTION", "true").lower() == "true"
|
||||||
|
|
||||||
# Tools available for future autonomy features
|
# Tools available for future autonomy features
|
||||||
TOOLS_AVAILABLE = ["RAG", "WEB", "WEATHER", "CODEBRAIN", "POKERBRAIN"]
|
TOOLS_AVAILABLE = ["RAG", "WEB", "WEATHER", "CODEBRAIN", "POKERBRAIN"]
|
||||||
@@ -39,34 +44,18 @@ SESSION_STATE: Dict[str, Dict[str, Any]] = {}
|
|||||||
# Logger
|
# Logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Set logging level based on VERBOSE_DEBUG
|
# Always set up basic logging
|
||||||
if VERBOSE_DEBUG:
|
logger.setLevel(logging.INFO)
|
||||||
logger.setLevel(logging.DEBUG)
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(logging.Formatter(
|
||||||
# Console handler
|
'%(asctime)s [CONTEXT] %(levelname)s: %(message)s',
|
||||||
console_handler = logging.StreamHandler()
|
datefmt='%H:%M:%S'
|
||||||
console_handler.setFormatter(logging.Formatter(
|
))
|
||||||
'%(asctime)s [CONTEXT] %(levelname)s: %(message)s',
|
logger.addHandler(console_handler)
|
||||||
datefmt='%H:%M:%S'
|
|
||||||
))
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
# File handler - append to log file
|
|
||||||
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 [CONTEXT] %(levelname)s: %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
))
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
logger.debug("VERBOSE_DEBUG mode enabled for context.py - logging to file")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"VERBOSE_DEBUG mode enabled for context.py - file logging failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Session initialization
|
# Session initialization & cleanup
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
def _init_session(session_id: str) -> Dict[str, Any]:
|
def _init_session(session_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -86,9 +75,76 @@ def _init_session(session_id: str) -> Dict[str, Any]:
|
|||||||
"active_project": None, # Future: project context
|
"active_project": None, # Future: project context
|
||||||
"message_count": 0,
|
"message_count": 0,
|
||||||
"message_history": [],
|
"message_history": [],
|
||||||
|
"last_message_hash": None, # For duplicate detection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_expired_sessions():
|
||||||
|
"""Remove sessions that haven't been active for SESSION_TTL_HOURS"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
expired_sessions = []
|
||||||
|
|
||||||
|
for session_id, state in SESSION_STATE.items():
|
||||||
|
last_active = state.get("last_timestamp", state.get("created_at"))
|
||||||
|
time_since_active = (now - last_active).total_seconds() / 3600 # hours
|
||||||
|
|
||||||
|
if time_since_active > SESSION_TTL_HOURS:
|
||||||
|
expired_sessions.append(session_id)
|
||||||
|
|
||||||
|
for session_id in expired_sessions:
|
||||||
|
del SESSION_STATE[session_id]
|
||||||
|
logger.info(f"🗑️ Expired session: {session_id} (inactive for {SESSION_TTL_HOURS}+ hours)")
|
||||||
|
|
||||||
|
return len(expired_sessions)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_duplicate_message(session_id: str, user_prompt: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if this message is a duplicate of the last processed message.
|
||||||
|
|
||||||
|
Uses simple hash comparison to detect exact duplicates or processing loops.
|
||||||
|
"""
|
||||||
|
if not ENABLE_DUPLICATE_DETECTION:
|
||||||
|
return False
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
state = SESSION_STATE.get(session_id)
|
||||||
|
if not state:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create hash of normalized message
|
||||||
|
message_hash = hashlib.md5(user_prompt.strip().lower().encode()).hexdigest()
|
||||||
|
|
||||||
|
# Check if it matches the last message
|
||||||
|
if state.get("last_message_hash") == message_hash:
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ DUPLICATE MESSAGE DETECTED | Session: {session_id} | "
|
||||||
|
f"Message: {user_prompt[:80]}..."
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Update hash for next check
|
||||||
|
state["last_message_hash"] = message_hash
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_message_history(state: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Trim message history to prevent unbounded growth.
|
||||||
|
|
||||||
|
Keeps only the most recent MAX_MESSAGE_HISTORY messages.
|
||||||
|
"""
|
||||||
|
history = state.get("message_history", [])
|
||||||
|
|
||||||
|
if len(history) > MAX_MESSAGE_HISTORY:
|
||||||
|
trimmed_count = len(history) - MAX_MESSAGE_HISTORY
|
||||||
|
state["message_history"] = history[-MAX_MESSAGE_HISTORY:]
|
||||||
|
logger.info(f"✂️ Trimmed {trimmed_count} old messages from session {state['session_id']}")
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Intake context retrieval
|
# Intake context retrieval
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
@@ -223,26 +279,42 @@ async def collect_context(session_id: str, user_prompt: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# A. Initialize session state if needed
|
# A. Cleanup expired sessions periodically (every 100th call)
|
||||||
|
import random
|
||||||
|
if random.randint(1, 100) == 1:
|
||||||
|
_cleanup_expired_sessions()
|
||||||
|
|
||||||
|
# B. Initialize session state if needed
|
||||||
if session_id not in SESSION_STATE:
|
if session_id not in SESSION_STATE:
|
||||||
SESSION_STATE[session_id] = _init_session(session_id)
|
SESSION_STATE[session_id] = _init_session(session_id)
|
||||||
logger.info(f"Initialized new session: {session_id}")
|
logger.info(f"Initialized new session: {session_id}")
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[COLLECT_CONTEXT] New session state: {SESSION_STATE[session_id]}")
|
|
||||||
|
|
||||||
state = SESSION_STATE[session_id]
|
state = SESSION_STATE[session_id]
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
# C. Check for duplicate messages (loop detection)
|
||||||
logger.debug(f"[COLLECT_CONTEXT] Session {session_id} - User prompt: {user_prompt[:100]}...")
|
if _is_duplicate_message(session_id, user_prompt):
|
||||||
|
# Return cached context with warning flag
|
||||||
|
logger.warning(f"🔁 LOOP DETECTED - Returning cached context to prevent processing duplicate")
|
||||||
|
context_state = {
|
||||||
|
"session_id": session_id,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"minutes_since_last_msg": 0,
|
||||||
|
"message_count": state["message_count"],
|
||||||
|
"intake": {},
|
||||||
|
"rag": [],
|
||||||
|
"mode": state["mode"],
|
||||||
|
"mood": state["mood"],
|
||||||
|
"active_project": state["active_project"],
|
||||||
|
"tools_available": TOOLS_AVAILABLE,
|
||||||
|
"duplicate_detected": True,
|
||||||
|
}
|
||||||
|
return context_state
|
||||||
|
|
||||||
# B. Calculate time delta
|
# B. Calculate time delta
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
time_delta_seconds = (now - state["last_timestamp"]).total_seconds()
|
time_delta_seconds = (now - state["last_timestamp"]).total_seconds()
|
||||||
minutes_since_last_msg = round(time_delta_seconds / 60.0, 2)
|
minutes_since_last_msg = round(time_delta_seconds / 60.0, 2)
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[COLLECT_CONTEXT] Time since last message: {minutes_since_last_msg:.2f} minutes")
|
|
||||||
|
|
||||||
# C. Gather Intake context (multilevel summaries)
|
# C. Gather Intake context (multilevel summaries)
|
||||||
# Build compact message buffer for Intake:
|
# Build compact message buffer for Intake:
|
||||||
messages_for_intake = []
|
messages_for_intake = []
|
||||||
@@ -257,12 +329,6 @@ async def collect_context(session_id: str, user_prompt: str) -> Dict[str, Any]:
|
|||||||
|
|
||||||
intake_data = await _get_intake_context(session_id, messages_for_intake)
|
intake_data = await _get_intake_context(session_id, messages_for_intake)
|
||||||
|
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
import json
|
|
||||||
logger.debug(f"[COLLECT_CONTEXT] Intake data retrieved:")
|
|
||||||
logger.debug(json.dumps(intake_data, indent=2, default=str))
|
|
||||||
|
|
||||||
# D. Search NeoMem for relevant memories
|
# D. Search NeoMem for relevant memories
|
||||||
if NEOMEM_ENABLED:
|
if NEOMEM_ENABLED:
|
||||||
rag_results = await _search_neomem(
|
rag_results = await _search_neomem(
|
||||||
@@ -274,23 +340,20 @@ async def collect_context(session_id: str, user_prompt: str) -> Dict[str, Any]:
|
|||||||
rag_results = []
|
rag_results = []
|
||||||
logger.info("Skipping NeoMem RAG retrieval; NEOMEM_ENABLED is false")
|
logger.info("Skipping NeoMem RAG retrieval; NEOMEM_ENABLED is false")
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[COLLECT_CONTEXT] NeoMem search returned {len(rag_results)} results")
|
|
||||||
for idx, result in enumerate(rag_results, 1):
|
|
||||||
score = result.get("score", 0)
|
|
||||||
data_preview = str(result.get("payload", {}).get("data", ""))[:100]
|
|
||||||
logger.debug(f" [{idx}] Score: {score:.3f} - {data_preview}...")
|
|
||||||
|
|
||||||
# E. Update session state
|
# E. Update session state
|
||||||
state["last_user_message"] = user_prompt
|
state["last_user_message"] = user_prompt
|
||||||
state["last_timestamp"] = now
|
state["last_timestamp"] = now
|
||||||
state["message_count"] += 1
|
state["message_count"] += 1
|
||||||
|
|
||||||
# Save user turn to history
|
# Save user turn to history
|
||||||
state["message_history"].append({
|
state["message_history"].append({
|
||||||
"user": user_prompt,
|
"user": user_prompt,
|
||||||
"assistant": "" # assistant reply filled later by update_last_assistant_message()
|
"assistant": "" # assistant reply filled later by update_last_assistant_message()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Trim history to prevent unbounded growth
|
||||||
|
_trim_message_history(state)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# F. Assemble unified context
|
# F. Assemble unified context
|
||||||
@@ -307,18 +370,54 @@ async def collect_context(session_id: str, user_prompt: str) -> Dict[str, Any]:
|
|||||||
"tools_available": TOOLS_AVAILABLE,
|
"tools_available": TOOLS_AVAILABLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Log context summary in structured format
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Context collected for session {session_id}: "
|
f"📊 Context | Session: {session_id} | "
|
||||||
f"{len(rag_results)} RAG results, "
|
f"Messages: {state['message_count']} | "
|
||||||
f"{minutes_since_last_msg:.1f} minutes since last message"
|
f"Last: {minutes_since_last_msg:.1f}min | "
|
||||||
|
f"RAG: {len(rag_results)} results"
|
||||||
)
|
)
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
# Show detailed context in detailed/verbose mode
|
||||||
logger.debug(f"[COLLECT_CONTEXT] Final context state assembled:")
|
if LOG_DETAIL_LEVEL in ["detailed", "verbose"]:
|
||||||
logger.debug(f" - Message count: {state['message_count']}")
|
import json
|
||||||
logger.debug(f" - Mode: {state['mode']}, Mood: {state['mood']}")
|
logger.info(f"\n{'─'*100}")
|
||||||
logger.debug(f" - Active project: {state['active_project']}")
|
logger.info(f"[CONTEXT] Session {session_id} | User: {user_prompt[:80]}...")
|
||||||
logger.debug(f" - Tools available: {TOOLS_AVAILABLE}")
|
logger.info(f"{'─'*100}")
|
||||||
|
logger.info(f" Mode: {state['mode']} | Mood: {state['mood']} | Project: {state['active_project']}")
|
||||||
|
logger.info(f" Tools: {', '.join(TOOLS_AVAILABLE)}")
|
||||||
|
|
||||||
|
# Show intake summaries (condensed)
|
||||||
|
if intake_data:
|
||||||
|
logger.info(f"\n ╭─ INTAKE SUMMARIES ────────────────────────────────────────────────")
|
||||||
|
for level in ["L1", "L5", "L10", "L20", "L30"]:
|
||||||
|
if level in intake_data:
|
||||||
|
summary = intake_data[level]
|
||||||
|
if isinstance(summary, dict):
|
||||||
|
summary_text = summary.get("summary", str(summary)[:100])
|
||||||
|
else:
|
||||||
|
summary_text = str(summary)[:100]
|
||||||
|
logger.info(f" │ {level:4s}: {summary_text}...")
|
||||||
|
logger.info(f" ╰───────────────────────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
# Show RAG results (condensed)
|
||||||
|
if rag_results:
|
||||||
|
logger.info(f"\n ╭─ RAG RESULTS ({len(rag_results)}) ──────────────────────────────────────────────")
|
||||||
|
for idx, result in enumerate(rag_results[:5], 1): # Show top 5
|
||||||
|
score = result.get("score", 0)
|
||||||
|
data_preview = str(result.get("payload", {}).get("data", ""))[:60]
|
||||||
|
logger.info(f" │ [{idx}] {score:.3f} | {data_preview}...")
|
||||||
|
if len(rag_results) > 5:
|
||||||
|
logger.info(f" │ ... and {len(rag_results) - 5} more results")
|
||||||
|
logger.info(f" ╰───────────────────────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
# Show full raw data only in verbose mode
|
||||||
|
if LOG_DETAIL_LEVEL == "verbose":
|
||||||
|
logger.info(f"\n ╭─ RAW INTAKE DATA ─────────────────────────────────────────────────")
|
||||||
|
logger.info(f" │ {json.dumps(intake_data, indent=4, default=str)}")
|
||||||
|
logger.info(f" ╰───────────────────────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
logger.info(f"{'─'*100}\n")
|
||||||
|
|
||||||
return context_state
|
return context_state
|
||||||
|
|
||||||
@@ -346,9 +445,6 @@ def update_last_assistant_message(session_id: str, message: str) -> None:
|
|||||||
# history entry already contains {"user": "...", "assistant": "...?"}
|
# history entry already contains {"user": "...", "assistant": "...?"}
|
||||||
history[-1]["assistant"] = message
|
history[-1]["assistant"] = message
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"Updated assistant message for session {session_id}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_session_state(session_id: str) -> Optional[Dict[str, Any]]:
|
def get_session_state(session_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"focus": "user_request",
|
"focus": "user_request",
|
||||||
"confidence": 0.7,
|
"confidence": 0.7,
|
||||||
"curiosity": 1.0,
|
"curiosity": 1.0,
|
||||||
"last_updated": "2025-12-19T20:25:25.437557",
|
"last_updated": "2025-12-20T07:47:53.826587",
|
||||||
"interaction_count": 16,
|
"interaction_count": 20,
|
||||||
"learning_queue": [],
|
"learning_queue": [],
|
||||||
"active_goals": [],
|
"active_goals": [],
|
||||||
"preferences": {
|
"preferences": {
|
||||||
|
|||||||
193
cortex/router.py
193
cortex/router.py
@@ -20,30 +20,17 @@ from autonomy.self.state import load_self_state
|
|||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Setup
|
# Setup
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
VERBOSE_DEBUG = os.getenv("VERBOSE_DEBUG", "false").lower() == "true"
|
LOG_DETAIL_LEVEL = os.getenv("LOG_DETAIL_LEVEL", "summary").lower()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
# Always set up basic logging
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.INFO)
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
console_handler = logging.StreamHandler()
|
console_handler.setFormatter(logging.Formatter(
|
||||||
console_handler.setFormatter(logging.Formatter(
|
'%(asctime)s [ROUTER] %(levelname)s: %(message)s',
|
||||||
'%(asctime)s [ROUTER] %(levelname)s: %(message)s',
|
datefmt='%H:%M:%S'
|
||||||
datefmt='%H:%M:%S'
|
))
|
||||||
))
|
logger.addHandler(console_handler)
|
||||||
logger.addHandler(console_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 [ROUTER] %(levelname)s: %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
))
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
logger.debug("VERBOSE_DEBUG enabled for router.py")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"File logging failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
cortex_router = APIRouter()
|
cortex_router = APIRouter()
|
||||||
@@ -64,40 +51,36 @@ class ReasonRequest(BaseModel):
|
|||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
@cortex_router.post("/reason")
|
@cortex_router.post("/reason")
|
||||||
async def run_reason(req: ReasonRequest):
|
async def run_reason(req: ReasonRequest):
|
||||||
|
from datetime import datetime
|
||||||
|
pipeline_start = datetime.now()
|
||||||
|
stage_timings = {}
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
# Show pipeline start in detailed/verbose mode
|
||||||
logger.debug(f"\n{'='*80}")
|
if LOG_DETAIL_LEVEL in ["detailed", "verbose"]:
|
||||||
logger.debug(f"[PIPELINE START] Session: {req.session_id}")
|
logger.info(f"\n{'='*100}")
|
||||||
logger.debug(f"[PIPELINE START] User prompt: {req.user_prompt[:200]}...")
|
logger.info(f"🚀 PIPELINE START | Session: {req.session_id} | {datetime.now().strftime('%H:%M:%S.%f')[:-3]}")
|
||||||
logger.debug(f"{'='*80}\n")
|
logger.info(f"{'='*100}")
|
||||||
|
logger.info(f"📝 User: {req.user_prompt[:150]}...")
|
||||||
|
logger.info(f"{'-'*100}\n")
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 0 — Context
|
# STAGE 0 — Context
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
if VERBOSE_DEBUG:
|
stage_start = datetime.now()
|
||||||
logger.debug("[STAGE 0] Collecting unified context...")
|
|
||||||
|
|
||||||
context_state = await collect_context(req.session_id, req.user_prompt)
|
context_state = await collect_context(req.session_id, req.user_prompt)
|
||||||
|
stage_timings["context"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[STAGE 0] Context collected - {len(context_state.get('rag', []))} RAG results")
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 0.5 — Identity
|
# STAGE 0.5 — Identity
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
if VERBOSE_DEBUG:
|
stage_start = datetime.now()
|
||||||
logger.debug("[STAGE 0.5] Loading identity block...")
|
|
||||||
|
|
||||||
identity_block = load_identity(req.session_id)
|
identity_block = load_identity(req.session_id)
|
||||||
|
stage_timings["identity"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[STAGE 0.5] Identity loaded: {identity_block.get('name', 'Unknown')}")
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 0.6 — Inner Monologue (observer-only)
|
# STAGE 0.6 — Inner Monologue (observer-only)
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
if VERBOSE_DEBUG:
|
stage_start = datetime.now()
|
||||||
logger.debug("[STAGE 0.6] Running inner monologue...")
|
|
||||||
|
|
||||||
inner_result = None
|
inner_result = None
|
||||||
try:
|
try:
|
||||||
@@ -111,21 +94,22 @@ async def run_reason(req: ReasonRequest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner_result = await inner_monologue.process(mono_context)
|
inner_result = await inner_monologue.process(mono_context)
|
||||||
logger.info(f"[INNER_MONOLOGUE] {inner_result}")
|
logger.info(f"🧠 Monologue | {inner_result.get('intent', 'unknown')} | Tone: {inner_result.get('tone', 'neutral')}")
|
||||||
|
|
||||||
# Store in context for downstream use
|
# Store in context for downstream use
|
||||||
context_state["monologue"] = inner_result
|
context_state["monologue"] = inner_result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[INNER_MONOLOGUE] failed: {e}")
|
logger.warning(f"⚠️ Monologue failed: {e}")
|
||||||
|
|
||||||
|
stage_timings["monologue"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 0.7 — Executive Planning (conditional)
|
# STAGE 0.7 — Executive Planning (conditional)
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
stage_start = datetime.now()
|
||||||
executive_plan = None
|
executive_plan = None
|
||||||
if inner_result and inner_result.get("consult_executive"):
|
if inner_result and inner_result.get("consult_executive"):
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 0.7] Executive consultation requested...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from autonomy.executive.planner import plan_execution
|
from autonomy.executive.planner import plan_execution
|
||||||
@@ -135,21 +119,22 @@ async def run_reason(req: ReasonRequest):
|
|||||||
context_state=context_state,
|
context_state=context_state,
|
||||||
identity_block=identity_block
|
identity_block=identity_block
|
||||||
)
|
)
|
||||||
logger.info(f"[EXECUTIVE] Generated plan: {executive_plan.get('summary', 'N/A')}")
|
logger.info(f"🎯 Executive plan: {executive_plan.get('summary', 'N/A')[:80]}...")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[EXECUTIVE] Planning failed: {e}")
|
logger.warning(f"⚠️ Executive planning failed: {e}")
|
||||||
executive_plan = None
|
executive_plan = None
|
||||||
|
|
||||||
|
stage_timings["executive"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 0.8 — Autonomous Tool Invocation
|
# STAGE 0.8 — Autonomous Tool Invocation
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
stage_start = datetime.now()
|
||||||
tool_results = None
|
tool_results = None
|
||||||
autonomous_enabled = os.getenv("ENABLE_AUTONOMOUS_TOOLS", "true").lower() == "true"
|
autonomous_enabled = os.getenv("ENABLE_AUTONOMOUS_TOOLS", "true").lower() == "true"
|
||||||
tool_confidence_threshold = float(os.getenv("AUTONOMOUS_TOOL_CONFIDENCE_THRESHOLD", "0.6"))
|
tool_confidence_threshold = float(os.getenv("AUTONOMOUS_TOOL_CONFIDENCE_THRESHOLD", "0.6"))
|
||||||
|
|
||||||
if autonomous_enabled and inner_result:
|
if autonomous_enabled and inner_result:
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 0.8] Analyzing autonomous tool needs...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from autonomy.tools.decision_engine import ToolDecisionEngine
|
from autonomy.tools.decision_engine import ToolDecisionEngine
|
||||||
@@ -176,22 +161,25 @@ async def run_reason(req: ReasonRequest):
|
|||||||
tool_context = orchestrator.format_results_for_context(tool_results)
|
tool_context = orchestrator.format_results_for_context(tool_results)
|
||||||
context_state["autonomous_tool_results"] = tool_context
|
context_state["autonomous_tool_results"] = tool_context
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
summary = tool_results.get("execution_summary", {})
|
||||||
summary = tool_results.get("execution_summary", {})
|
logger.info(f"🛠️ Tools executed: {summary.get('successful', [])} succeeded")
|
||||||
logger.debug(f"[STAGE 0.8] Tools executed: {summary.get('successful', [])} succeeded")
|
|
||||||
else:
|
else:
|
||||||
if VERBOSE_DEBUG:
|
logger.info(f"🛠️ No tools invoked (confidence: {tool_decision.get('confidence', 0):.2f})")
|
||||||
logger.debug(f"[STAGE 0.8] No tools invoked (confidence: {tool_decision.get('confidence', 0):.2f})")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[STAGE 0.8] Autonomous tool invocation failed: {e}")
|
logger.warning(f"⚠️ Autonomous tool invocation failed: {e}")
|
||||||
if VERBOSE_DEBUG:
|
if LOG_DETAIL_LEVEL == "verbose":
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
stage_timings["tools"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 1 — Intake summary
|
# STAGE 1-5 — Core Reasoning Pipeline
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
stage_start = datetime.now()
|
||||||
|
|
||||||
|
# Extract intake summary
|
||||||
intake_summary = "(no context available)"
|
intake_summary = "(no context available)"
|
||||||
if context_state.get("intake"):
|
if context_state.get("intake"):
|
||||||
l20 = context_state["intake"].get("L20")
|
l20 = context_state["intake"].get("L20")
|
||||||
@@ -200,65 +188,46 @@ async def run_reason(req: ReasonRequest):
|
|||||||
elif isinstance(l20, str):
|
elif isinstance(l20, str):
|
||||||
intake_summary = l20
|
intake_summary = l20
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
# Reflection
|
||||||
logger.debug(f"[STAGE 1] Intake summary extracted (L20): {intake_summary[:150]}...")
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
# STAGE 2 — Reflection
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 2] Running reflection...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reflection = await reflect_notes(intake_summary, identity_block=identity_block)
|
reflection = await reflect_notes(intake_summary, identity_block=identity_block)
|
||||||
reflection_notes = reflection.get("notes", [])
|
reflection_notes = reflection.get("notes", [])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
reflection_notes = []
|
reflection_notes = []
|
||||||
if VERBOSE_DEBUG:
|
logger.warning(f"⚠️ Reflection failed: {e}")
|
||||||
logger.debug(f"[STAGE 2] Reflection failed: {e}")
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
stage_timings["reflection"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
# STAGE 3 — Reasoning (draft)
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 3] Running reasoning (draft)...")
|
|
||||||
|
|
||||||
|
# Reasoning (draft)
|
||||||
|
stage_start = datetime.now()
|
||||||
draft = await reason_check(
|
draft = await reason_check(
|
||||||
req.user_prompt,
|
req.user_prompt,
|
||||||
identity_block=identity_block,
|
identity_block=identity_block,
|
||||||
rag_block=context_state.get("rag", []),
|
rag_block=context_state.get("rag", []),
|
||||||
reflection_notes=reflection_notes,
|
reflection_notes=reflection_notes,
|
||||||
context=context_state,
|
context=context_state,
|
||||||
monologue=inner_result, # NEW: Pass monologue guidance
|
monologue=inner_result,
|
||||||
executive_plan=executive_plan # NEW: Pass executive plan
|
executive_plan=executive_plan
|
||||||
)
|
)
|
||||||
|
stage_timings["reasoning"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# Refinement
|
||||||
# STAGE 4 — Refinement
|
stage_start = datetime.now()
|
||||||
# ----------------------------------------------------------------
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 4] Running refinement...")
|
|
||||||
|
|
||||||
result = await refine_answer(
|
result = await refine_answer(
|
||||||
draft_output=draft,
|
draft_output=draft,
|
||||||
reflection_notes=reflection_notes,
|
reflection_notes=reflection_notes,
|
||||||
identity_block=identity_block,
|
identity_block=identity_block,
|
||||||
rag_block=context_state.get("rag", []),
|
rag_block=context_state.get("rag", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
final_neutral = result["final_output"]
|
final_neutral = result["final_output"]
|
||||||
|
stage_timings["refinement"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# Persona
|
||||||
# STAGE 5 — Persona
|
stage_start = datetime.now()
|
||||||
# ----------------------------------------------------------------
|
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug("[STAGE 5] Applying persona layer...")
|
|
||||||
|
|
||||||
# Extract tone and depth from monologue for persona guidance
|
|
||||||
tone = inner_result.get("tone", "neutral") if inner_result else "neutral"
|
tone = inner_result.get("tone", "neutral") if inner_result else "neutral"
|
||||||
depth = inner_result.get("depth", "medium") if inner_result else "medium"
|
depth = inner_result.get("depth", "medium") if inner_result else "medium"
|
||||||
|
|
||||||
persona_answer = await speak(final_neutral, tone=tone, depth=depth)
|
persona_answer = await speak(final_neutral, tone=tone, depth=depth)
|
||||||
|
stage_timings["persona"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 6 — Session update
|
# STAGE 6 — Session update
|
||||||
@@ -268,6 +237,7 @@ async def run_reason(req: ReasonRequest):
|
|||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 6.5 — Self-state update & Pattern Learning
|
# STAGE 6.5 — Self-state update & Pattern Learning
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
stage_start = datetime.now()
|
||||||
try:
|
try:
|
||||||
from autonomy.self.analyzer import analyze_and_update_state
|
from autonomy.self.analyzer import analyze_and_update_state
|
||||||
await analyze_and_update_state(
|
await analyze_and_update_state(
|
||||||
@@ -277,9 +247,8 @@ async def run_reason(req: ReasonRequest):
|
|||||||
context=context_state
|
context=context_state
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[SELF_STATE] Update failed: {e}")
|
logger.warning(f"⚠️ Self-state update failed: {e}")
|
||||||
|
|
||||||
# Pattern learning
|
|
||||||
try:
|
try:
|
||||||
from autonomy.learning.pattern_learner import get_pattern_learner
|
from autonomy.learning.pattern_learner import get_pattern_learner
|
||||||
learner = get_pattern_learner()
|
learner = get_pattern_learner()
|
||||||
@@ -290,11 +259,14 @@ async def run_reason(req: ReasonRequest):
|
|||||||
context=context_state
|
context=context_state
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[PATTERN_LEARNER] Learning failed: {e}")
|
logger.warning(f"⚠️ Pattern learning failed: {e}")
|
||||||
|
|
||||||
|
stage_timings["learning"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# STAGE 7 — Proactive Monitoring & Suggestions
|
# STAGE 7 — Proactive Monitoring & Suggestions
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
stage_start = datetime.now()
|
||||||
proactive_enabled = os.getenv("ENABLE_PROACTIVE_MONITORING", "true").lower() == "true"
|
proactive_enabled = os.getenv("ENABLE_PROACTIVE_MONITORING", "true").lower() == "true"
|
||||||
proactive_min_priority = float(os.getenv("PROACTIVE_SUGGESTION_MIN_PRIORITY", "0.6"))
|
proactive_min_priority = float(os.getenv("PROACTIVE_SUGGESTION_MIN_PRIORITY", "0.6"))
|
||||||
|
|
||||||
@@ -303,7 +275,7 @@ async def run_reason(req: ReasonRequest):
|
|||||||
from autonomy.proactive.monitor import get_proactive_monitor
|
from autonomy.proactive.monitor import get_proactive_monitor
|
||||||
|
|
||||||
monitor = get_proactive_monitor(min_priority=proactive_min_priority)
|
monitor = get_proactive_monitor(min_priority=proactive_min_priority)
|
||||||
self_state = load_self_state() # Already imported at top of file
|
self_state = load_self_state()
|
||||||
|
|
||||||
suggestion = await monitor.analyze_session(
|
suggestion = await monitor.analyze_session(
|
||||||
session_id=req.session_id,
|
session_id=req.session_id,
|
||||||
@@ -311,22 +283,35 @@ async def run_reason(req: ReasonRequest):
|
|||||||
self_state=self_state
|
self_state=self_state
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append suggestion to response if exists
|
|
||||||
if suggestion:
|
if suggestion:
|
||||||
suggestion_text = monitor.format_suggestion(suggestion)
|
suggestion_text = monitor.format_suggestion(suggestion)
|
||||||
persona_answer += suggestion_text
|
persona_answer += suggestion_text
|
||||||
|
logger.info(f"💡 Proactive suggestion: {suggestion['type']} (priority: {suggestion['priority']:.2f})")
|
||||||
if VERBOSE_DEBUG:
|
|
||||||
logger.debug(f"[STAGE 7] Proactive suggestion added: {suggestion['type']} (priority: {suggestion['priority']:.2f})")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[STAGE 7] Proactive monitoring failed: {e}")
|
logger.warning(f"⚠️ Proactive monitoring failed: {e}")
|
||||||
|
|
||||||
if VERBOSE_DEBUG:
|
stage_timings["proactive"] = (datetime.now() - stage_start).total_seconds() * 1000
|
||||||
logger.debug(f"\n{'='*80}")
|
|
||||||
logger.debug(f"[PIPELINE COMPLETE] Session: {req.session_id}")
|
# ----------------------------------------------------------------
|
||||||
logger.debug(f"[PIPELINE COMPLETE] Final answer length: {len(persona_answer)} chars")
|
# PIPELINE COMPLETE — Summary
|
||||||
logger.debug(f"{'='*80}\n")
|
# ----------------------------------------------------------------
|
||||||
|
total_duration = (datetime.now() - pipeline_start).total_seconds() * 1000
|
||||||
|
|
||||||
|
# Always show pipeline completion
|
||||||
|
logger.info(f"\n{'='*100}")
|
||||||
|
logger.info(f"✨ PIPELINE COMPLETE | Session: {req.session_id} | Total: {total_duration:.0f}ms")
|
||||||
|
logger.info(f"{'='*100}")
|
||||||
|
|
||||||
|
# Show timing breakdown in detailed/verbose mode
|
||||||
|
if LOG_DETAIL_LEVEL in ["detailed", "verbose"]:
|
||||||
|
logger.info("⏱️ Stage Timings:")
|
||||||
|
for stage, duration in stage_timings.items():
|
||||||
|
pct = (duration / total_duration) * 100 if total_duration > 0 else 0
|
||||||
|
logger.info(f" {stage:15s}: {duration:6.0f}ms ({pct:5.1f}%)")
|
||||||
|
|
||||||
|
logger.info(f"📤 Output: {len(persona_answer)} chars")
|
||||||
|
logger.info(f"{'='*100}\n")
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# RETURN
|
# RETURN
|
||||||
|
|||||||
223
cortex/utils/logging_utils.py
Normal file
223
cortex/utils/logging_utils.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
"""
|
||||||
|
Structured logging utilities for Cortex pipeline debugging.
|
||||||
|
|
||||||
|
Provides hierarchical, scannable logs with clear section markers and raw data visibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class LogLevel(Enum):
|
||||||
|
"""Log detail levels"""
|
||||||
|
MINIMAL = 1 # Only errors and final results
|
||||||
|
SUMMARY = 2 # Stage summaries + errors
|
||||||
|
DETAILED = 3 # Include raw LLM outputs, RAG results
|
||||||
|
VERBOSE = 4 # Everything including intermediate states
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineLogger:
|
||||||
|
"""
|
||||||
|
Hierarchical logger for cortex pipeline debugging.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Clear visual section markers
|
||||||
|
- Collapsible detail sections
|
||||||
|
- Raw data dumps with truncation options
|
||||||
|
- Stage timing
|
||||||
|
- Error highlighting
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger: logging.Logger, level: LogLevel = LogLevel.SUMMARY):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
self.stage_timings = {}
|
||||||
|
self.current_stage = None
|
||||||
|
self.stage_start_time = None
|
||||||
|
self.pipeline_start_time = None
|
||||||
|
|
||||||
|
def pipeline_start(self, session_id: str, user_prompt: str):
|
||||||
|
"""Mark the start of a pipeline run"""
|
||||||
|
self.pipeline_start_time = datetime.now()
|
||||||
|
self.stage_timings = {}
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
self.logger.info(f"\n{'='*100}")
|
||||||
|
self.logger.info(f"🚀 PIPELINE START | Session: {session_id} | {datetime.now().strftime('%H:%M:%S.%f')[:-3]}")
|
||||||
|
self.logger.info(f"{'='*100}")
|
||||||
|
if self.level.value >= LogLevel.DETAILED.value:
|
||||||
|
self.logger.info(f"📝 User prompt: {user_prompt[:200]}{'...' if len(user_prompt) > 200 else ''}")
|
||||||
|
self.logger.info(f"{'-'*100}\n")
|
||||||
|
|
||||||
|
def stage_start(self, stage_name: str, description: str = ""):
|
||||||
|
"""Mark the start of a pipeline stage"""
|
||||||
|
self.current_stage = stage_name
|
||||||
|
self.stage_start_time = datetime.now()
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||||
|
desc_suffix = f" - {description}" if description else ""
|
||||||
|
self.logger.info(f"▶️ [{stage_name}]{desc_suffix} | {timestamp}")
|
||||||
|
|
||||||
|
def stage_end(self, result_summary: str = ""):
|
||||||
|
"""Mark the end of a pipeline stage"""
|
||||||
|
if self.current_stage and self.stage_start_time:
|
||||||
|
duration_ms = (datetime.now() - self.stage_start_time).total_seconds() * 1000
|
||||||
|
self.stage_timings[self.current_stage] = duration_ms
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
summary_suffix = f" → {result_summary}" if result_summary else ""
|
||||||
|
self.logger.info(f"✅ [{self.current_stage}] Complete in {duration_ms:.0f}ms{summary_suffix}\n")
|
||||||
|
|
||||||
|
self.current_stage = None
|
||||||
|
self.stage_start_time = None
|
||||||
|
|
||||||
|
def log_llm_call(self, backend: str, prompt: str, response: Any, raw_response: str = None):
|
||||||
|
"""
|
||||||
|
Log LLM call details with proper formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backend: Backend name (PRIMARY, SECONDARY, etc.)
|
||||||
|
prompt: Input prompt to LLM
|
||||||
|
response: Parsed response object
|
||||||
|
raw_response: Raw JSON response string
|
||||||
|
"""
|
||||||
|
if self.level.value >= LogLevel.DETAILED.value:
|
||||||
|
self.logger.info(f" 🧠 LLM Call | Backend: {backend}")
|
||||||
|
|
||||||
|
# Show prompt (truncated)
|
||||||
|
if isinstance(prompt, list):
|
||||||
|
prompt_preview = prompt[-1].get('content', '')[:150] if prompt else ''
|
||||||
|
else:
|
||||||
|
prompt_preview = str(prompt)[:150]
|
||||||
|
self.logger.info(f" Prompt: {prompt_preview}...")
|
||||||
|
|
||||||
|
# Show parsed response
|
||||||
|
if isinstance(response, dict):
|
||||||
|
response_text = (
|
||||||
|
response.get('reply') or
|
||||||
|
response.get('message', {}).get('content') or
|
||||||
|
str(response)
|
||||||
|
)[:200]
|
||||||
|
else:
|
||||||
|
response_text = str(response)[:200]
|
||||||
|
|
||||||
|
self.logger.info(f" Response: {response_text}...")
|
||||||
|
|
||||||
|
# Show raw response in collapsible block
|
||||||
|
if raw_response and self.level.value >= LogLevel.VERBOSE.value:
|
||||||
|
self.logger.debug(f" ╭─ RAW RESPONSE ────────────────────────────────────")
|
||||||
|
for line in raw_response.split('\n')[:50]: # Limit to 50 lines
|
||||||
|
self.logger.debug(f" │ {line}")
|
||||||
|
if raw_response.count('\n') > 50:
|
||||||
|
self.logger.debug(f" │ ... ({raw_response.count(chr(10)) - 50} more lines)")
|
||||||
|
self.logger.debug(f" ╰───────────────────────────────────────────────────\n")
|
||||||
|
|
||||||
|
def log_rag_results(self, results: List[Dict[str, Any]]):
|
||||||
|
"""Log RAG/NeoMem results in scannable format"""
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
self.logger.info(f" 📚 RAG Results: {len(results)} memories retrieved")
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.DETAILED.value and results:
|
||||||
|
self.logger.info(f" ╭─ MEMORY SCORES ───────────────────────────────────")
|
||||||
|
for idx, result in enumerate(results[:10], 1): # Show top 10
|
||||||
|
score = result.get("score", 0)
|
||||||
|
data_preview = str(result.get("payload", {}).get("data", ""))[:80]
|
||||||
|
self.logger.info(f" │ [{idx}] {score:.3f} | {data_preview}...")
|
||||||
|
if len(results) > 10:
|
||||||
|
self.logger.info(f" │ ... and {len(results) - 10} more results")
|
||||||
|
self.logger.info(f" ╰───────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
def log_context_state(self, context_state: Dict[str, Any]):
|
||||||
|
"""Log context state summary"""
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
msg_count = context_state.get("message_count", 0)
|
||||||
|
minutes_since = context_state.get("minutes_since_last_msg", 0)
|
||||||
|
rag_count = len(context_state.get("rag", []))
|
||||||
|
|
||||||
|
self.logger.info(f" 📊 Context | Messages: {msg_count} | Last: {minutes_since:.1f}min ago | RAG: {rag_count} results")
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.DETAILED.value:
|
||||||
|
intake = context_state.get("intake", {})
|
||||||
|
if intake:
|
||||||
|
self.logger.info(f" ╭─ INTAKE SUMMARIES ────────────────────────────────")
|
||||||
|
for level in ["L1", "L5", "L10", "L20", "L30"]:
|
||||||
|
if level in intake:
|
||||||
|
summary = intake[level]
|
||||||
|
if isinstance(summary, dict):
|
||||||
|
summary = summary.get("summary", str(summary)[:100])
|
||||||
|
else:
|
||||||
|
summary = str(summary)[:100]
|
||||||
|
self.logger.info(f" │ {level}: {summary}...")
|
||||||
|
self.logger.info(f" ╰───────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
def log_error(self, stage: str, error: Exception, critical: bool = False):
|
||||||
|
"""Log an error with context"""
|
||||||
|
level_marker = "🔴 CRITICAL" if critical else "⚠️ WARNING"
|
||||||
|
self.logger.error(f"{level_marker} | Stage: {stage} | Error: {type(error).__name__}: {str(error)}")
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.VERBOSE.value:
|
||||||
|
import traceback
|
||||||
|
self.logger.debug(f" Traceback:\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def log_raw_data(self, label: str, data: Any, max_lines: int = 30):
|
||||||
|
"""Log raw data in a collapsible format"""
|
||||||
|
if self.level.value >= LogLevel.VERBOSE.value:
|
||||||
|
self.logger.debug(f" ╭─ {label.upper()} ──────────────────────────────────")
|
||||||
|
|
||||||
|
if isinstance(data, (dict, list)):
|
||||||
|
json_str = json.dumps(data, indent=2, default=str)
|
||||||
|
lines = json_str.split('\n')
|
||||||
|
for line in lines[:max_lines]:
|
||||||
|
self.logger.debug(f" │ {line}")
|
||||||
|
if len(lines) > max_lines:
|
||||||
|
self.logger.debug(f" │ ... ({len(lines) - max_lines} more lines)")
|
||||||
|
else:
|
||||||
|
lines = str(data).split('\n')
|
||||||
|
for line in lines[:max_lines]:
|
||||||
|
self.logger.debug(f" │ {line}")
|
||||||
|
if len(lines) > max_lines:
|
||||||
|
self.logger.debug(f" │ ... ({len(lines) - max_lines} more lines)")
|
||||||
|
|
||||||
|
self.logger.debug(f" ╰───────────────────────────────────────────────────")
|
||||||
|
|
||||||
|
def pipeline_end(self, session_id: str, final_output_length: int):
|
||||||
|
"""Mark the end of pipeline run with summary"""
|
||||||
|
if self.pipeline_start_time:
|
||||||
|
total_duration_ms = (datetime.now() - self.pipeline_start_time).total_seconds() * 1000
|
||||||
|
|
||||||
|
if self.level.value >= LogLevel.SUMMARY.value:
|
||||||
|
self.logger.info(f"\n{'='*100}")
|
||||||
|
self.logger.info(f"✨ PIPELINE COMPLETE | Session: {session_id} | Total: {total_duration_ms:.0f}ms")
|
||||||
|
self.logger.info(f"{'='*100}")
|
||||||
|
|
||||||
|
# Show timing breakdown
|
||||||
|
if self.stage_timings and self.level.value >= LogLevel.DETAILED.value:
|
||||||
|
self.logger.info("⏱️ Stage Timings:")
|
||||||
|
for stage, duration in self.stage_timings.items():
|
||||||
|
pct = (duration / total_duration_ms) * 100 if total_duration_ms > 0 else 0
|
||||||
|
self.logger.info(f" {stage:20s}: {duration:6.0f}ms ({pct:5.1f}%)")
|
||||||
|
|
||||||
|
self.logger.info(f"📤 Final output: {final_output_length} characters")
|
||||||
|
self.logger.info(f"{'='*100}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def get_log_level_from_env() -> LogLevel:
|
||||||
|
"""Parse log level from environment variable"""
|
||||||
|
import os
|
||||||
|
verbose_debug = os.getenv("VERBOSE_DEBUG", "false").lower() == "true"
|
||||||
|
detail_level = os.getenv("LOG_DETAIL_LEVEL", "").lower()
|
||||||
|
|
||||||
|
if detail_level == "minimal":
|
||||||
|
return LogLevel.MINIMAL
|
||||||
|
elif detail_level == "summary":
|
||||||
|
return LogLevel.SUMMARY
|
||||||
|
elif detail_level == "detailed":
|
||||||
|
return LogLevel.DETAILED
|
||||||
|
elif detail_level == "verbose" or verbose_debug:
|
||||||
|
return LogLevel.VERBOSE
|
||||||
|
else:
|
||||||
|
return LogLevel.SUMMARY # Default
|
||||||
@@ -10,75 +10,75 @@ volumes:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
# ============================================================
|
# # ============================================================
|
||||||
# NeoMem: Postgres
|
# # NeoMem: Postgres
|
||||||
# ============================================================
|
# # ============================================================
|
||||||
neomem-postgres:
|
# neomem-postgres:
|
||||||
image: ankane/pgvector:v0.5.1
|
# image: ankane/pgvector:v0.5.1
|
||||||
container_name: neomem-postgres
|
# container_name: neomem-postgres
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
environment:
|
# environment:
|
||||||
POSTGRES_USER: neomem
|
# POSTGRES_USER: neomem
|
||||||
POSTGRES_PASSWORD: neomempass
|
# POSTGRES_PASSWORD: neomempass
|
||||||
POSTGRES_DB: neomem
|
# POSTGRES_DB: neomem
|
||||||
volumes:
|
# volumes:
|
||||||
- ./volumes/postgres_data:/var/lib/postgresql/data
|
# - ./volumes/postgres_data:/var/lib/postgresql/data
|
||||||
ports:
|
# ports:
|
||||||
- "5432:5432"
|
# - "5432:5432"
|
||||||
healthcheck:
|
# healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U neomem -d neomem || exit 1"]
|
# test: ["CMD-SHELL", "pg_isready -U neomem -d neomem || exit 1"]
|
||||||
interval: 5s
|
# interval: 5s
|
||||||
timeout: 5s
|
# timeout: 5s
|
||||||
retries: 10
|
# retries: 10
|
||||||
networks:
|
# networks:
|
||||||
- lyra_net
|
# - lyra_net
|
||||||
|
|
||||||
# ============================================================
|
# # ============================================================
|
||||||
# NeoMem: Neo4j Graph
|
# # NeoMem: Neo4j Graph
|
||||||
# ============================================================
|
# # ============================================================
|
||||||
neomem-neo4j:
|
# neomem-neo4j:
|
||||||
image: neo4j:5
|
# image: neo4j:5
|
||||||
container_name: neomem-neo4j
|
# container_name: neomem-neo4j
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
environment:
|
# environment:
|
||||||
NEO4J_AUTH: "neo4j/neomemgraph"
|
# NEO4J_AUTH: "neo4j/neomemgraph"
|
||||||
NEO4JLABS_PLUGINS: '["graph-data-science"]'
|
# NEO4JLABS_PLUGINS: '["graph-data-science"]'
|
||||||
volumes:
|
# volumes:
|
||||||
- ./volumes/neo4j_data:/data
|
# - ./volumes/neo4j_data:/data
|
||||||
ports:
|
# ports:
|
||||||
- "7474:7474"
|
# - "7474:7474"
|
||||||
- "7687:7687"
|
# - "7687:7687"
|
||||||
healthcheck:
|
# healthcheck:
|
||||||
test: ["CMD-SHELL", "cypher-shell -u neo4j -p neomemgraph 'RETURN 1' || exit 1"]
|
# test: ["CMD-SHELL", "cypher-shell -u neo4j -p neomemgraph 'RETURN 1' || exit 1"]
|
||||||
interval: 10s
|
# interval: 10s
|
||||||
timeout: 10s
|
# timeout: 10s
|
||||||
retries: 10
|
# retries: 10
|
||||||
networks:
|
# networks:
|
||||||
- lyra_net
|
# - lyra_net
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# NeoMem API
|
# NeoMem API
|
||||||
# ============================================================
|
# ============================================================
|
||||||
neomem-api:
|
# neomem-api:
|
||||||
build:
|
# build:
|
||||||
context: ./neomem
|
# context: ./neomem
|
||||||
image: lyra-neomem:latest
|
# image: lyra-neomem:latest
|
||||||
container_name: neomem-api
|
# container_name: neomem-api
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
env_file:
|
# env_file:
|
||||||
- ./neomem/.env
|
# - ./neomem/.env
|
||||||
- ./.env
|
# - ./.env
|
||||||
volumes:
|
# volumes:
|
||||||
- ./neomem_history:/app/history
|
# - ./neomem_history:/app/history
|
||||||
ports:
|
# ports:
|
||||||
- "7077:7077"
|
# - "7077:7077"
|
||||||
depends_on:
|
# depends_on:
|
||||||
neomem-postgres:
|
# neomem-postgres:
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
neomem-neo4j:
|
# neomem-neo4j:
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
networks:
|
# networks:
|
||||||
- lyra_net
|
# - lyra_net
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Relay (host mode)
|
# Relay (host mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user