// Package forage — babytalk.go implements gap-to-query translation. // // WeakRegionWalk walks the lattice starting from weakly-locked nodes // (LockInWeight == 1), collecting the string values it passes through. // ExtractBabyTalk filters and deduplicates those fragments into a // search query — the lattice's "baby talk" about what it needs. package forage import ( "math/rand/v2" "sort" "strings" "git.mleku.dev/mleku/dendrite/pkg/lattice" ) // WeakRegionWalk performs a random walk starting from a weakly-locked node // in the lattice. It preferentially visits weak regions to discover what // the lattice hasn't fully crystallized yet. // // Returns the string values of occupied nodes visited during the walk. func WeakRegionWalk(l *lattice.Lattice, wc *lattice.WalkColumns, maxSteps int, seed uint64) []string { if wc == nil || wc.N == 0 { return nil } rng := rand.New(rand.NewPCG(seed, seed^0xcafe)) // Find a weak starting node: occupied with LockInWeight == 1. var startID uint32 found := false for attempts := 0; attempts < wc.N && attempts < 10000; attempts++ { id := uint32(rng.IntN(wc.N)) if wc.IsOccupied(lattice.NodeID(id)) && wc.LockInWeight(lattice.NodeID(id)) == 1 { startID = id found = true break } } if !found { // Fallback: any occupied node. for attempts := 0; attempts < wc.N && attempts < 10000; attempts++ { id := uint32(rng.IntN(wc.N)) if wc.IsOccupied(lattice.NodeID(id)) { startID = id found = true break } } } if !found { return nil } nodes := l.Nodes() values := make([]string, 0, maxSteps) type wn struct { id uint32 weight int64 } wnBuf := make([]wn, 0, 16) currentID := startID for range maxSteps { if wc.IsOccupied(lattice.NodeID(currentID)) { occ := nodes[currentID].Occupant() if occ != nil { if v, ok := occ.Value().(string); ok { values = append(values, v) } } } edges := wc.NeighborsOf(lattice.NodeID(currentID)) if len(edges) == 0 { currentID = uint32(rng.IntN(wc.N)) continue } // Prefer weak neighbors (inverse weighting: weight = 1/lockIn). wnBuf = wnBuf[:0] var totalWeight int64 for _, nbID := range edges { if wc.IsOccupied(lattice.NodeID(nbID)) { li := wc.LockInWeight(lattice.NodeID(nbID)) // Inverse: weaker nodes get higher walk probability. w := int64(1) if li > 0 { w = max(1, 10/li) } wnBuf = append(wnBuf, wn{nbID, w}) totalWeight += w } } if len(wnBuf) > 0 && totalWeight > 0 { pick := rng.Int64N(totalWeight) var cumul int64 for _, w := range wnBuf { cumul += w.weight if pick < cumul { currentID = w.id break } } } else if len(wnBuf) > 0 { currentID = wnBuf[rng.IntN(len(wnBuf))].id } else { currentID = edges[rng.IntN(len(edges))] } } return values } // ExtractBabyTalk filters walked values into a search query string. // It keeps only morpheme-length fragments (not single chars, spaces, or // punctuation), deduplicates, takes the longest unique ones, and joins // them into a query. // // Returns empty string if no usable fragments found (caller should use // random Gutenberg fallback). func ExtractBabyTalk(walkedValues []string) string { seen := make(map[string]bool, len(walkedValues)) var fragments []string for _, v := range walkedValues { v = strings.TrimSpace(v) if len(v) < 3 { continue } // Skip whitespace-only and punctuation-only fragments. if isAllSpace(v) || isAllPunct(v) { continue } if !seen[v] { seen[v] = true fragments = append(fragments, v) } } if len(fragments) == 0 { return "" } // Sort by length descending — longest fragments are most informative. sort.Slice(fragments, func(i, j int) bool { return len(fragments[i]) > len(fragments[j]) }) // Keep all unique fragments — let the lattice speak as much as it finds. return strings.Join(fragments, " ") } func isAllSpace(s string) bool { for _, r := range s { if r != ' ' && r != '\t' && r != '\n' && r != '\r' { return false } } return true } func isAllPunct(s string) bool { for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r > 127 { return false } } return true }