Codette-Reasoning / reasoning_forge /memory_weighting.py
Raiff1982's picture
Upload 120 files
ed1b365 verified
"""Memory-Weighted Adapter Selection for Phase 2
Learns which adapters perform best from historical memory data,
then weights adapter selection based on coherence, conflict success,
and recency of past interactions.
Author: Claude Code
Phase: 2 (Closed-Loop Learning)
"""
import time
import math
import json
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional, Tuple
# ================================================================
# Shared Utility Functions
# ================================================================
def clamp_adapter_weight(weight: float, min_val: float = 0.0, max_val: float = 2.0) -> float:
"""Clamp adapter weight to valid range.
Prevents unbounded amplification and ensures all weights stay within
[min_val, max_val] bounds, typically [0, 2.0].
Args:
weight: Weight value to clamp
min_val: Minimum allowed weight (default 0.0)
max_val: Maximum allowed weight (default 2.0)
Returns:
Clamped weight in [min_val, max_val]
"""
return max(min_val, min(max_val, weight))
@dataclass
class ReinforcementConfig:
"""Tunable coefficients for adapter reinforcement learning (Phase 4).
These control how much adapter weights are boosted/penalized based on
conflict resolution performance during debate rounds.
"""
boost_successful: float = 0.08 # Boost when resolution_rate > 40%
penalize_failed: float = 0.08 # Penalize when resolution_type == "worsened"
reward_soft_consensus: float = 0.03 # Partial reward for soft_consensus
@classmethod
def from_dict(cls, d: Dict) -> "ReinforcementConfig":
"""Create from config dict with defaults for missing keys."""
return cls(**{k: v for k, v in d.items()
if k in cls.__dataclass_fields__})
def to_dict(self) -> Dict:
"""Export as dict for serialization."""
return asdict(self)
@dataclass
class AdapterWeight:
"""Performance metrics for a single adapter based on historical memory."""
adapter: str # Adapter name (e.g., "newton", "davinci")
base_coherence: float # Mean coherence [0, 1] from all past uses
conflict_success_rate: float # % of "tension"-tagged memories with coherence > 0.7
interaction_count: int # How many memories for this adapter
recency_score: float # Recent memories weighted higher [0.1, 1.0]
weight: float # Final composite weight [0, 2.0]
def __str__(self) -> str:
return (f"AdapterWeight(adapter={self.adapter}, "
f"coherence={self.base_coherence:.3f}, "
f"conflict_success={self.conflict_success_rate:.1%}, "
f"interactions={self.interaction_count}, "
f"weight={self.weight:.3f})")
class MemoryWeighting:
"""
Score adapter performance and weight selection decisions.
Aggregates memory cocoons per adapter, computes weights based on:
- base_coherence: Mean coherence across all uses
- conflict_success_rate: % of high-tension memories → resolved well
- recency: Recent memories weighted higher (exponential decay, ~7 day half-life)
Weight range [0, 2.0]:
- 0.5: Adapter performs poorly (suppress by 50%)
- 1.0: Average performance (neutral)
- 2.0: Excellent adapter (boost by 100%)
"""
def __init__(self, living_memory, update_interval_hours: float = 1.0,
reinforcement_config: Optional[ReinforcementConfig] = None):
"""
Initialize memory weighting engine.
Args:
living_memory: LivingMemoryKernel instance with cocoons
update_interval_hours: Recompute weights every N hours
reinforcement_config: Phase 4 tunable coefficients (boost/penalize amounts)
"""
self.memory = living_memory
self.update_interval_hours = update_interval_hours
self.reinforcement_config = reinforcement_config or ReinforcementConfig()
self.adapter_weights: Dict[str, AdapterWeight] = {}
self.last_updated: float = 0.0
self._compute_weights(force_recompute=True)
def get_reinforcement_config(self) -> Dict:
"""Return current reinforcement coefficient values for tuning."""
return self.reinforcement_config.to_dict()
def set_reinforcement_config(self, config_dict: Dict) -> None:
"""Update reinforcement coefficients from dict. Useful for fine-tuning."""
self.reinforcement_config = ReinforcementConfig.from_dict(config_dict)
def compute_weights(self, force_recompute: bool = False) -> Dict[str, float]:
"""
Aggregate memory cocoons per adapter and compute weights.
Weights can be used to:
1. Boost/suppress keyword router confidence
2. Rerank adapters during selection
3. Explain adapter decisions
Returns:
Dict[adapter_name: weight_float] where weight ∈ [0, 2.0]
"""
return self._compute_weights(force_recompute)
def _compute_weights(self, force_recompute: bool = False) -> Dict[str, float]:
"""Compute weights for all adapters in memory."""
# Skip if already computed recently (unless forced)
now = time.time()
if not force_recompute and (now - self.last_updated) < (self.update_interval_hours * 3600):
return {a: w.weight for a, w in self.adapter_weights.items()}
# Group cocoons by adapter
adapter_cocoons: Dict[str, List] = {}
if self.memory and self.memory.memories:
for cocoon in self.memory.memories:
if cocoon.adapter_used:
# Handle compound adapter names like "Newton,DaVinci"
adapters = [a.strip().lower() for a in cocoon.adapter_used.split(",")]
for adapter in adapters:
if adapter:
adapter_cocoons.setdefault(adapter, []).append(cocoon)
# Compute weights for each adapter
self.adapter_weights = {}
if not adapter_cocoons:
# No memories yet - return neutral weights
return {}
adapter_names = list(adapter_cocoons.keys())
for adapter in adapter_names:
cocoons = adapter_cocoons[adapter]
# 1. Base coherence: mean coherence from all uses
coherences = [c.coherence for c in cocoons if c.coherence > 0]
base_coherence = sum(coherences) / len(coherences) if coherences else 0.5
# 2. Conflict success rate: % of tension memories with coherence > 0.7
tension_memories = [c for c in cocoons if c.emotional_tag == "tension"]
if tension_memories:
successful = sum(1 for c in tension_memories if c.coherence > 0.7)
conflict_success_rate = successful / len(tension_memories)
else:
conflict_success_rate = 0.5 # No conflict history yet
# 3. Recency score: weight recent memories higher
# Using exponential decay with ~7 day half-life
recency_weights = []
for cocoon in cocoons:
age_hours = cocoon.age_hours()
# exp(-age_hours / 168) = 0.5 after 1 week
recency = math.exp(-age_hours / 168.0)
recency_weights.append(recency)
avg_recency = sum(recency_weights) / len(recency_weights) if recency_weights else 0.5
recency_score = 0.1 + 0.9 * avg_recency # Map to [0.1, 1.0]
# 4. Composite weight: [0, 2.0]
# weight = 1.0 + contributions from each signal
# - base_coherence contributes ±0.5
# - conflict_success contributes ±0.3
# - recency contributes ±0.2
weight = (
1.0 +
0.5 * (base_coherence - 0.5) * 2.0 + # Normalize to [-0.5, 0.5]
0.3 * (conflict_success_rate - 0.5) * 2.0 +
0.2 * (recency_score - 0.5) * 2.0
)
# Clamp to [0, 2.0]
weight = clamp_adapter_weight(weight)
self.adapter_weights[adapter] = AdapterWeight(
adapter=adapter,
base_coherence=base_coherence,
conflict_success_rate=conflict_success_rate,
interaction_count=len(cocoons),
recency_score=recency_score,
weight=weight,
)
self.last_updated = now
return {a: w.weight for a, w in self.adapter_weights.items()}
def select_primary(self, conflict_type: str = "", query: str = "") -> Tuple[str, float]:
"""
Select primary adapter for a conflict context.
Strategy:
1. Find adapters that historically handled this conflict_type well
(Search memories with emotional_tag="tension" AND conflict_type in content)
2. Rank by AdapterWeight.conflict_success_rate descending
3. Return (adapter_name, weight)
Args:
conflict_type: e.g., "contradiction", "emphasis", "framework"
query: Optional query context for semantic matching
Returns:
(best_adapter_name, weight_score)
"""
if not self.adapter_weights:
return ("", 1.0) # No history yet
# Find tension cocoons matching conflict_type if provided
if conflict_type and self.memory and self.memory.memories:
conflict_type_lower = conflict_type.lower()
tension_cocoons = [
c for c in self.memory.memories
if c.emotional_tag == "tension" and conflict_type_lower in c.content.lower()
]
# Score adapters by conflict success on matching memories
if tension_cocoons:
adapter_conflict_success = {}
for cocoon in tension_cocoons:
for adapter_str in cocoon.adapter_used.split(","):
adapter = adapter_str.strip().lower()
if adapter:
success = cocoon.coherence > 0.7
adapter_conflict_success.setdefault(adapter, []).append(success)
# Rank by success rate
best_adapter = None
best_score = 0.0
for adapter, successes in adapter_conflict_success.items():
success_rate = sum(successes) / len(successes) if successes else 0.5
if success_rate > best_score:
best_adapter = adapter
best_score = success_rate
if best_adapter and best_adapter in self.adapter_weights:
return (best_adapter, self.adapter_weights[best_adapter].weight)
# Fallback: return adapter with highest overall weight
if self.adapter_weights:
best = max(self.adapter_weights.items(), key=lambda x: x[1].weight)
return (best[0], best[1].weight)
return ("", 1.0)
def get_boosted_confidence(self, adapter: str, base_confidence: float) -> float:
"""
Modulate keyword router confidence based on memory history.
Formula:
boosted = base_confidence * (1.0 + weight_modifier)
where weight_modifier = (weight - 1.0) / 2.0 → [-0.5, +0.5]
High-performing adapters (weight=2.0) get +50% confidence boost.
Low-performing adapters (weight=0.0) get -50% confidence reduction.
Args:
adapter: Adapter name
base_confidence: Original confidence from keyword router [0, 1]
Returns:
Boosted confidence, clamped to [0, 1]
"""
if adapter not in self.adapter_weights:
return base_confidence # No history for this adapter
weight = self.adapter_weights[adapter].weight
# Convert weight [0, 2] to modifier [-0.5, +0.5]
weight_modifier = (weight - 1.0) / 2.0
# Apply modifier
boosted = base_confidence * (1.0 + weight_modifier)
# Clamp to [0, 1]
return max(0.0, min(1.0, boosted))
def explain_weight(self, adapter: str) -> Dict[str, float]:
"""
Explain how weight was computed for debugging/transparency.
Returns breakdown of coherence, conflict success, recency components.
"""
if adapter not in self.adapter_weights:
return {"error": f"No history for adapter '{adapter}'"}
w = self.adapter_weights[adapter]
return {
"adapter": w.adapter,
"base_coherence": w.base_coherence,
"conflict_success_rate": w.conflict_success_rate,
"recency_score": w.recency_score,
"interaction_count": w.interaction_count,
"final_weight": w.weight,
"explanation": (
f"Adapter '{w.adapter}' has used {w.interaction_count} times with "
f"{w.base_coherence:.1%} avg coherence, {w.conflict_success_rate:.0%} "
f"conflict resolution rate, and {w.recency_score:.1%} recency score. "
f"Final weight: {w.weight:.3f} (range [0, 2.0])"
)
}
def get_all_weights(self) -> Dict[str, Dict]:
"""Get detailed weight breakdown for all adapters."""
result = {}
for adapter, weight in self.adapter_weights.items():
result[adapter] = {
"weight": weight.weight,
"coherence": weight.base_coherence,
"conflict_success": weight.conflict_success_rate,
"recency": weight.recency_score,
"uses": weight.interaction_count,
}
return result
def get_summary(self) -> Dict:
"""Get summary stats of adapter weighting engine."""
if not self.adapter_weights:
return {"message": "No memories yet, weights will initialize on first use"}
weights = [w.weight for w in self.adapter_weights.values()]
coherences = [w.base_coherence for w in self.adapter_weights.values()]
return {
"total_adapters": len(self.adapter_weights),
"total_memories": len(self.memory.memories) if self.memory else 0,
"avg_weight": sum(weights) / len(weights) if weights else 1.0,
"best_adapter": max(self.adapter_weights.items(), key=lambda x: x[1].weight)[0] if self.adapter_weights else "none",
"avg_coherence": sum(coherences) / len(coherences) if coherences else 0.0,
"last_updated": self.last_updated,
}
# ========================================================================
# Phase 4: Self-Correcting Feedback Loop
# ========================================================================
def boost(self, adapter: str, amount: float = 0.05):
"""Boost adapter weight for successful resolution."""
adapter_lower = adapter.lower()
if adapter_lower in self.adapter_weights:
self.adapter_weights[adapter_lower].weight += amount
# Clamp to [0, 2.0]
self.adapter_weights[adapter_lower].weight = clamp_adapter_weight(
self.adapter_weights[adapter_lower].weight
)
def penalize(self, adapter: str, amount: float = 0.05):
"""Penalize adapter weight for failed resolution."""
adapter_lower = adapter.lower()
if adapter_lower in self.adapter_weights:
self.adapter_weights[adapter_lower].weight -= amount
# Clamp to [0, 2.0]
self.adapter_weights[adapter_lower].weight = max(
0.0, min(2.0, self.adapter_weights[adapter_lower].weight)
)
def update_from_evolution(self, evolution) -> Dict[str, float]:
"""
Update adapter weights based on conflict resolution performance.
Reinforcement learning: boost adapters that resolved conflicts well,
penalize those that made things worse.
Uses coefficients from self.reinforcement_config for tuning.
Args:
evolution: ConflictEvolution object with resolution_rate and type
Returns:
Dict with boost/penalize actions taken
"""
agents = [
evolution.original_conflict.agent_a.lower(),
evolution.original_conflict.agent_b.lower(),
]
actions = {"boosts": [], "penalties": []}
# Reward successful resolution (configurable threshold and amount)
if evolution.resolution_rate > 0.4:
for agent in agents:
self.boost(agent, amount=self.reinforcement_config.boost_successful)
actions["boosts"].append(agent)
# Penalize failure (configurable)
elif evolution.resolution_type == "worsened":
for agent in agents:
self.penalize(agent, amount=self.reinforcement_config.penalize_failed)
actions["penalties"].append(agent)
# Slight reward for soft consensus (configurable)
elif evolution.resolution_type == "soft_consensus":
for agent in agents:
self.boost(agent, amount=self.reinforcement_config.reward_soft_consensus)
actions["boosts"].append(agent)
return actions