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