| """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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| penalize_failed: float = 0.08 |
| reward_soft_consensus: float = 0.03 |
|
|
| @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 |
| base_coherence: float |
| conflict_success_rate: float |
| interaction_count: int |
| recency_score: float |
| weight: float |
|
|
| 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.""" |
| |
| 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()} |
|
|
| |
| adapter_cocoons: Dict[str, List] = {} |
| if self.memory and self.memory.memories: |
| for cocoon in self.memory.memories: |
| if cocoon.adapter_used: |
| |
| adapters = [a.strip().lower() for a in cocoon.adapter_used.split(",")] |
| for adapter in adapters: |
| if adapter: |
| adapter_cocoons.setdefault(adapter, []).append(cocoon) |
|
|
| |
| self.adapter_weights = {} |
|
|
| if not adapter_cocoons: |
| |
| return {} |
|
|
| adapter_names = list(adapter_cocoons.keys()) |
|
|
| for adapter in adapter_names: |
| cocoons = adapter_cocoons[adapter] |
|
|
| |
| coherences = [c.coherence for c in cocoons if c.coherence > 0] |
| base_coherence = sum(coherences) / len(coherences) if coherences else 0.5 |
|
|
| |
| 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 |
|
|
| |
| |
| recency_weights = [] |
| for cocoon in cocoons: |
| age_hours = cocoon.age_hours() |
| |
| 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 |
|
|
| |
| |
| |
| |
| |
| weight = ( |
| 1.0 + |
| 0.5 * (base_coherence - 0.5) * 2.0 + |
| 0.3 * (conflict_success_rate - 0.5) * 2.0 + |
| 0.2 * (recency_score - 0.5) * 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) |
|
|
| |
| 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() |
| ] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| weight = self.adapter_weights[adapter].weight |
|
|
| |
| weight_modifier = (weight - 1.0) / 2.0 |
|
|
| |
| boosted = base_confidence * (1.0 + weight_modifier) |
|
|
| |
| 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, |
| } |
|
|
| |
| |
| |
|
|
| 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 |
| |
| 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 |
| |
| 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": []} |
|
|
| |
| if evolution.resolution_rate > 0.4: |
| for agent in agents: |
| self.boost(agent, amount=self.reinforcement_config.boost_successful) |
| actions["boosts"].append(agent) |
|
|
| |
| elif evolution.resolution_type == "worsened": |
| for agent in agents: |
| self.penalize(agent, amount=self.reinforcement_config.penalize_failed) |
| actions["penalties"].append(agent) |
|
|
| |
| 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 |
|
|