Fractus / src /fractus /auto_growth.py
thefinalboss's picture
Upload src/fractus/auto_growth.py with huggingface_hub
451a96c verified
Raw
History Blame Contribute Delete
14.3 kB
"""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__()
# Tiny network: 5 input features → 5 actions
# Features: [loss_trend, expert_utilization_entropy, memory_size,
# domain_coverage, confidence_avg]
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)
# Heuristic policy (not learned — transparent and debuggable).
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)
# Decision tree.
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)
# Remove in reverse order (don't mess up indices).
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:
# Keep the longer one.
if len(self.kb.chunks[j]) > len(self.kb.chunks[i]):
to_keep.remove(i)
else:
to_keep.remove(j)
removed += 1
# Rebuild without duplicates.
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
# Length factor.
score += min(len(text) / 500.0, 0.3)
# Specificity: contains numbers?
if any(c.isdigit() for c in text):
score += 0.1
# Specificity: contains code-like patterns?
if "def " in text or "class " in text or "import " in text:
score += 0.2
# Specificity: contains factual statements?
factual_markers = [" is ", " are ", " was ", " created ", " built ", " made "]
if any(marker in text.lower() for marker in factual_markers):
score += 0.15
# Specificity: contains questions (less important to keep)?
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
# Score all memories.
scores = [(self.importance_score(text), i)
for i, text in enumerate(self.kb.chunks)]
scores.sort() # lowest importance first
# Remove the lowest-scoring ones.
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)
# Track metrics over time.
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 # add 32 experts per growth event
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":
# Consolidate + prune low importance.
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":
# This would be triggered by MetaCognition when it detects a correction.
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