babytalk.go raw

   1  // Package forage — babytalk.go implements gap-to-query translation.
   2  //
   3  // WeakRegionWalk walks the lattice starting from weakly-locked nodes
   4  // (LockInWeight == 1), collecting the string values it passes through.
   5  // ExtractBabyTalk filters and deduplicates those fragments into a
   6  // search query — the lattice's "baby talk" about what it needs.
   7  package forage
   8  
   9  import (
  10  	"math/rand/v2"
  11  	"sort"
  12  	"strings"
  13  
  14  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
  15  )
  16  
  17  // WeakRegionWalk performs a random walk starting from a weakly-locked node
  18  // in the lattice. It preferentially visits weak regions to discover what
  19  // the lattice hasn't fully crystallized yet.
  20  //
  21  // Returns the string values of occupied nodes visited during the walk.
  22  func WeakRegionWalk(l *lattice.Lattice, wc *lattice.WalkColumns, maxSteps int, seed uint64) []string {
  23  	if wc == nil || wc.N == 0 {
  24  		return nil
  25  	}
  26  
  27  	rng := rand.New(rand.NewPCG(seed, seed^0xcafe))
  28  
  29  	// Find a weak starting node: occupied with LockInWeight == 1.
  30  	var startID uint32
  31  	found := false
  32  	for attempts := 0; attempts < wc.N && attempts < 10000; attempts++ {
  33  		id := uint32(rng.IntN(wc.N))
  34  		if wc.IsOccupied(lattice.NodeID(id)) && wc.LockInWeight(lattice.NodeID(id)) == 1 {
  35  			startID = id
  36  			found = true
  37  			break
  38  		}
  39  	}
  40  	if !found {
  41  		// Fallback: any occupied node.
  42  		for attempts := 0; attempts < wc.N && attempts < 10000; attempts++ {
  43  			id := uint32(rng.IntN(wc.N))
  44  			if wc.IsOccupied(lattice.NodeID(id)) {
  45  				startID = id
  46  				found = true
  47  				break
  48  			}
  49  		}
  50  	}
  51  	if !found {
  52  		return nil
  53  	}
  54  
  55  	nodes := l.Nodes()
  56  	values := make([]string, 0, maxSteps)
  57  
  58  	type wn struct {
  59  		id     uint32
  60  		weight int64
  61  	}
  62  	wnBuf := make([]wn, 0, 16)
  63  
  64  	currentID := startID
  65  
  66  	for range maxSteps {
  67  		if wc.IsOccupied(lattice.NodeID(currentID)) {
  68  			occ := nodes[currentID].Occupant()
  69  			if occ != nil {
  70  				if v, ok := occ.Value().(string); ok {
  71  					values = append(values, v)
  72  				}
  73  			}
  74  		}
  75  
  76  		edges := wc.NeighborsOf(lattice.NodeID(currentID))
  77  		if len(edges) == 0 {
  78  			currentID = uint32(rng.IntN(wc.N))
  79  			continue
  80  		}
  81  
  82  		// Prefer weak neighbors (inverse weighting: weight = 1/lockIn).
  83  		wnBuf = wnBuf[:0]
  84  		var totalWeight int64
  85  		for _, nbID := range edges {
  86  			if wc.IsOccupied(lattice.NodeID(nbID)) {
  87  				li := wc.LockInWeight(lattice.NodeID(nbID))
  88  				// Inverse: weaker nodes get higher walk probability.
  89  				w := int64(1)
  90  				if li > 0 {
  91  					w = max(1, 10/li)
  92  				}
  93  				wnBuf = append(wnBuf, wn{nbID, w})
  94  				totalWeight += w
  95  			}
  96  		}
  97  
  98  		if len(wnBuf) > 0 && totalWeight > 0 {
  99  			pick := rng.Int64N(totalWeight)
 100  			var cumul int64
 101  			for _, w := range wnBuf {
 102  				cumul += w.weight
 103  				if pick < cumul {
 104  					currentID = w.id
 105  					break
 106  				}
 107  			}
 108  		} else if len(wnBuf) > 0 {
 109  			currentID = wnBuf[rng.IntN(len(wnBuf))].id
 110  		} else {
 111  			currentID = edges[rng.IntN(len(edges))]
 112  		}
 113  	}
 114  
 115  	return values
 116  }
 117  
 118  // ExtractBabyTalk filters walked values into a search query string.
 119  // It keeps only morpheme-length fragments (not single chars, spaces, or
 120  // punctuation), deduplicates, takes the longest unique ones, and joins
 121  // them into a query.
 122  //
 123  // Returns empty string if no usable fragments found (caller should use
 124  // random Gutenberg fallback).
 125  func ExtractBabyTalk(walkedValues []string) string {
 126  	seen := make(map[string]bool, len(walkedValues))
 127  	var fragments []string
 128  
 129  	for _, v := range walkedValues {
 130  		v = strings.TrimSpace(v)
 131  		if len(v) < 3 {
 132  			continue
 133  		}
 134  		// Skip whitespace-only and punctuation-only fragments.
 135  		if isAllSpace(v) || isAllPunct(v) {
 136  			continue
 137  		}
 138  		if !seen[v] {
 139  			seen[v] = true
 140  			fragments = append(fragments, v)
 141  		}
 142  	}
 143  
 144  	if len(fragments) == 0 {
 145  		return ""
 146  	}
 147  
 148  	// Sort by length descending — longest fragments are most informative.
 149  	sort.Slice(fragments, func(i, j int) bool {
 150  		return len(fragments[i]) > len(fragments[j])
 151  	})
 152  
 153  	// Keep all unique fragments — let the lattice speak as much as it finds.
 154  
 155  	return strings.Join(fragments, " ")
 156  }
 157  
 158  func isAllSpace(s string) bool {
 159  	for _, r := range s {
 160  		if r != ' ' && r != '\t' && r != '\n' && r != '\r' {
 161  			return false
 162  		}
 163  	}
 164  	return true
 165  }
 166  
 167  func isAllPunct(s string) bool {
 168  	for _, r := range s {
 169  		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r > 127 {
 170  			return false
 171  		}
 172  	}
 173  	return true
 174  }
 175