| """Fractus Self-Growth + Memory Management. |
| |
| Fractus decides ITSELF when to: |
| - Add experts (when it encounters a new domain it can't handle) |
| - Expand rank (when existing experts plateau) |
| - Forget memories (when they're wrong, outdated, or harmful) |
| - Modify memories (correct mistakes, update facts) |
| |
| This is the autonomous growth loop — the AGI layer. |
| """ |
| import os, sys, time, math |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import numpy as np |
| from typing import List, Tuple, Optional |
|
|
|
|
| class SelfGrowthPolicy(nn.Module): |
| """A small policy network that decides HOW Fractus should grow. |
| |
| Inputs: Fractus's current state (performance metrics, domain coverage, |
| expert utilization, memory size). |
| Outputs: growth actions (ADD_EXPERT, EXPAND_RANK, NOTHING). |
| |
| This is NOT trained by backprop — it uses a simple heuristic policy |
| that can be refined over time. The goal is autonomy, not optimization. |
| """ |
|
|
| ACTIONS = ["NOTHING", "ADD_EXPERT", "EXPAND_RANK", "FORGET", "MODIFY"] |
|
|
| def __init__(self, n_domains=10): |
| super().__init__() |
| |
| |
| |
| self.net = nn.Sequential( |
| nn.Linear(5, 16), |
| nn.ReLU(), |
| nn.Linear(16, len(self.ACTIONS)), |
| ) |
|
|
| def forward(self, features): |
| """features: (5,) tensor → action logits (5,)""" |
| return self.net(features) |
|
|
| def decide(self, features): |
| """Decide what to do based on current state. |
| |
| Args: |
| features: dict with keys: |
| - loss_trend: float (negative = improving, positive = degrading) |
| - expert_utilization: float (0-1, entropy of expert selection) |
| - memory_size: int (number of memories) |
| - domain_coverage: float (0-1, how many domains are covered) |
| - confidence_avg: float (0-1, average generation confidence) |
| |
| Returns: |
| action: str (one of ACTIONS) |
| reason: str (why this action was chosen) |
| """ |
| f = torch.tensor([ |
| features.get("loss_trend", 0.0), |
| features.get("expert_utilization", 0.5), |
| min(features.get("memory_size", 0) / 1000.0, 1.0), |
| features.get("domain_coverage", 0.5), |
| features.get("confidence_avg", 0.5), |
| ], dtype=torch.float32) |
|
|
| |
| loss_trend = features.get("loss_trend", 0.0) |
| util = features.get("expert_utilization", 0.5) |
| coverage = features.get("domain_coverage", 0.5) |
| mem_size = features.get("memory_size", 0) |
|
|
| |
| if loss_trend > 0.1 and coverage < 0.7: |
| return "ADD_EXPERT", f"Loss degrading ({loss_trend:+.2f}) and domain coverage low ({coverage:.0%}) — need new specialists" |
|
|
| if loss_trend > 0.1 and coverage >= 0.7: |
| return "EXPAND_RANK", f"Loss degrading ({loss_trend:+.2f}) but domains covered — existing experts need more depth" |
|
|
| if mem_size > 5000: |
| return "FORGET", f"Memory large ({mem_size} entries) — consolidate and forget irrelevant entries" |
|
|
| return "NOTHING", "Model is healthy — no growth needed" |
|
|
|
|
| class MemoryManager: |
| """Manages Fractus's persistent memory — store, retrieve, forget, modify. |
| |
| This wraps the KnowledgeBase with higher-level operations: |
| - forget(pattern): remove memories matching a pattern |
| - modify(old, new): replace an old memory with a corrected version |
| - consolidate(): merge similar memories, remove duplicates |
| - importance_score(memory): how important is this memory? |
| """ |
|
|
| def __init__(self, kb): |
| self.kb = kb |
|
|
| def forget(self, pattern: str = None, index: int = None, |
| source: str = None, older_than_days: int = None): |
| """Remove memories matching criteria. |
| |
| Args: |
| pattern: remove memories containing this text (case-insensitive) |
| index: remove the memory at this specific index |
| source: remove all memories from this source |
| older_than_days: remove memories older than N days |
| |
| Returns: |
| Number of memories removed. |
| """ |
| if not self.kb.chunks: |
| return 0 |
|
|
| to_keep = [] |
| removed = 0 |
|
|
| for i, (text, src) in enumerate(zip(self.kb.chunks, |
| self.kb.sources if self.kb.sources else [""] * len(self.kb.chunks))): |
| keep = True |
|
|
| if index is not None and i == index: |
| keep = False |
|
|
| if pattern and pattern.lower() in text.lower(): |
| keep = False |
|
|
| if source and src == source: |
| keep = False |
|
|
| if keep: |
| to_keep.append(i) |
|
|
| |
| to_remove = sorted([i for i in range(len(self.kb.chunks)) if i not in to_keep], |
| reverse=True) |
| for i in to_remove: |
| self.kb.chunks.pop(i) |
| if i < len(self.kb.embeddings): |
| self.kb.embeddings.pop(i) |
| if i < len(self.kb.sources): |
| self.kb.sources.pop(i) |
| removed += 1 |
|
|
| return removed |
|
|
| def modify(self, old_pattern: str, new_text: str, source: str = "correction"): |
| """Replace memories containing old_pattern with new_text. |
| |
| Args: |
| old_pattern: find memories containing this text |
| new_text: the corrected text to replace them with |
| source: source label for the correction |
| |
| Returns: |
| Number of memories modified. |
| """ |
| if not self.kb.chunks: |
| return 0 |
|
|
| modified = 0 |
| for i, text in enumerate(self.kb.chunks): |
| if old_pattern.lower() in text.lower(): |
| self.kb.chunks[i] = new_text |
| if i < len(self.kb.sources): |
| self.kb.sources[i] = source |
| modified += 1 |
|
|
| return modified |
|
|
| def consolidate(self, similarity_threshold: float = 0.9): |
| """Merge near-duplicate memories. |
| |
| Removes memories that are >90% similar to an earlier one. |
| Keeps the longer version. |
| |
| Returns: |
| Number of duplicates removed. |
| """ |
| if len(self.kb.chunks) < 2 or not self.kb.embeddings: |
| return 0 |
|
|
| removed = 0 |
| to_keep = list(range(len(self.kb.chunks))) |
|
|
| bank = np.array(self.kb.embeddings, dtype=np.float32) |
| norms = np.linalg.norm(bank, axis=1, keepdims=True) |
| normalized = bank / (norms + 1e-10) |
|
|
| for i in range(len(self.kb.chunks)): |
| if i not in to_keep: |
| continue |
| for j in range(i + 1, len(self.kb.chunks)): |
| if j not in to_keep: |
| continue |
| sim = float(normalized[i] @ normalized[j]) |
| if sim > similarity_threshold: |
| |
| if len(self.kb.chunks[j]) > len(self.kb.chunks[i]): |
| to_keep.remove(i) |
| else: |
| to_keep.remove(j) |
| removed += 1 |
|
|
| |
| self.kb.chunks = [self.kb.chunks[i] for i in sorted(to_keep)] |
| self.kb.embeddings = [self.kb.embeddings[i] for i in sorted(to_keep) |
| if i < len(self.kb.embeddings)] |
| self.kb.sources = [self.kb.sources[i] for i in sorted(to_keep) |
| if self.kb.sources and i < len(self.kb.sources)] |
|
|
| return removed |
|
|
| def importance_score(self, text: str) -> float: |
| """Estimate how important a memory is. |
| |
| Factors: |
| - Length (longer = more information) |
| - Specificity (contains proper nouns, numbers, code) |
| - Recency (if timestamp available) |
| |
| Returns: 0.0 to 1.0 |
| """ |
| score = 0.0 |
|
|
| |
| score += min(len(text) / 500.0, 0.3) |
|
|
| |
| if any(c.isdigit() for c in text): |
| score += 0.1 |
|
|
| |
| if "def " in text or "class " in text or "import " in text: |
| score += 0.2 |
|
|
| |
| factual_markers = [" is ", " are ", " was ", " created ", " built ", " made "] |
| if any(marker in text.lower() for marker in factual_markers): |
| score += 0.15 |
|
|
| |
| if "?" in text: |
| score -= 0.1 |
|
|
| return max(0.0, min(1.0, score)) |
|
|
| def prune_low_importance(self, max_memories: int = 1000): |
| """Remove least important memories when over the limit. |
| |
| Args: |
| max_memories: maximum memories to keep. |
| Returns: |
| Number removed. |
| """ |
| if len(self.kb.chunks) <= max_memories: |
| return 0 |
|
|
| |
| scores = [(self.importance_score(text), i) |
| for i, text in enumerate(self.kb.chunks)] |
| scores.sort() |
|
|
| |
| n_to_remove = len(self.kb.chunks) - max_memories |
| to_remove = set(idx for _, idx in scores[:n_to_remove]) |
|
|
| kept_chunks = [] |
| kept_embeddings = [] |
| kept_sources = [] |
|
|
| for i in range(len(self.kb.chunks)): |
| if i not in to_remove: |
| kept_chunks.append(self.kb.chunks[i]) |
| if i < len(self.kb.embeddings): |
| kept_embeddings.append(self.kb.embeddings[i]) |
| if self.kb.sources and i < len(self.kb.sources): |
| kept_sources.append(self.kb.sources[i]) |
|
|
| removed = len(self.kb.chunks) - len(kept_chunks) |
| self.kb.chunks = kept_chunks |
| self.kb.embeddings = kept_embeddings |
| self.kb.sources = kept_sources |
|
|
| return removed |
|
|
|
|
| class FractusSelfGrowth: |
| """The autonomous growth loop. |
| |
| Combines SelfGrowthPolicy + MemoryManager + FractusGrowth to let |
| Fractus decide how to improve itself. |
| |
| Usage: |
| self_growth = FractusSelfGrowth(model, tok, kb, device) |
| action = self_growth.evaluate(metrics) |
| if action: |
| self_growth.execute(action, data) |
| """ |
|
|
| def __init__(self, model, tokenizer, kb, device="cpu"): |
| self.model = model |
| self.tok = tokenizer |
| self.kb = kb |
| self.device = torch.device(device) |
| self.policy = SelfGrowthPolicy() |
| self.memory_mgr = MemoryManager(kb) |
|
|
| |
| self.loss_history = [] |
| self.expert_usage = {} |
|
|
| def evaluate(self, metrics: dict): |
| """Evaluate current state and decide if growth is needed. |
| |
| Args: |
| metrics: dict with current performance metrics. |
| |
| Returns: |
| dict with action + reason, or None if nothing to do. |
| """ |
| action, reason = self.policy.decide(metrics) |
|
|
| if action == "NOTHING": |
| return None |
|
|
| return { |
| "action": action, |
| "reason": reason, |
| "metrics": metrics, |
| } |
|
|
| def execute(self, decision: dict, data=None, domain=None): |
| """Execute a growth decision. |
| |
| Args: |
| decision: dict from evaluate() |
| data: token tensor for expert training (if ADD_EXPERT) |
| domain: domain name for new experts (if ADD_EXPERT) |
| """ |
| from fractus.growth import FractusGrowth |
|
|
| action = decision["action"] |
| reason = decision["reason"] |
|
|
| print(f"[SelfGrowth] Action: {action}", flush=True) |
| print(f"[SelfGrowth] Reason: {reason}", flush=True) |
|
|
| if action == "ADD_EXPERT": |
| growth = FractusGrowth(self.model, self.tok, str(self.device)) |
| n_new = 32 |
| growth.add_experts( |
| n_new=n_new, |
| data=data, |
| domain=domain or "auto_detected", |
| steps_per_expert=2000, |
| ) |
| print(f"[SelfGrowth] Added {n_new} experts per layer", flush=True) |
|
|
| elif action == "EXPAND_RANK": |
| growth = FractusGrowth(self.model, self.tok, str(self.device)) |
| current_rank = self.model.blocks[0].moe.experts_w1[0].rank |
| new_rank = current_rank * 2 |
| growth.expand_rank(target_rank=new_rank) |
| print(f"[SelfGrowth] Expanded rank {current_rank} → {new_rank}", flush=True) |
|
|
| elif action == "FORGET": |
| |
| duplicates = self.memory_mgr.consolidate() |
| pruned = self.memory_mgr.prune_low_importance(max_memories=500) |
| print(f"[SelfGrowth] Forgot {duplicates} duplicates + {pruned} low-importance memories", |
| flush=True) |
|
|
| elif action == "MODIFY": |
| |
| print(f"[SelfGrowth] Memory modification queued (use memory_mgr.modify() directly)", |
| flush=True) |
|
|
| def user_forget(self, pattern: str = None, source: str = None): |
| """User-requested forgetting. |
| |
| The user can tell Fractus to forget specific things. |
| """ |
| removed = self.memory_mgr.forget(pattern=pattern, source=source) |
| print(f"[SelfGrowth] Forgot {removed} memories matching '{pattern or source}'", flush=True) |
| return removed |
|
|
| def user_correct(self, old_pattern: str, new_text: str): |
| """User-requested correction. |
| |
| The user corrects a wrong memory. |
| """ |
| modified = self.memory_mgr.modify(old_pattern, new_text, source="user_correction") |
| print(f"[SelfGrowth] Corrected {modified} memories: '{old_pattern[:30]}' → '{new_text[:30]}'", |
| flush=True) |
| return modified |
|
|