Go · 7203 bytes Raw Blame History
1 package llm
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sync"
8 "time"
9 )
10
11 // EditDistanceMatcher finds similar past failures and adapts their insults
12 // Uses Levenshtein distance for command similarity matching
13 type EditDistanceMatcher struct {
14 mu sync.RWMutex
15 commandHistory []CommandRecord
16 maxHistory int
17 similarityThresh float64
18 persistencePath string
19 }
20
21 // CommandRecord stores a past command failure with its successful insult
22 type CommandRecord struct {
23 Command string
24 CommandType string
25 ErrorPattern string
26 ProjectType string
27 Timestamp time.Time
28 Insult string
29 Effectiveness float64 // How well this insult worked (from RL)
30 }
31
32 // SimilarCommand represents a similar past command
33 type SimilarCommand struct {
34 Record CommandRecord
35 Similarity float64
36 Distance int
37 }
38
39 // NewEditDistanceMatcher creates a new matcher
40 func NewEditDistanceMatcher() *EditDistanceMatcher {
41 homeDir, _ := os.UserHomeDir()
42 persistPath := filepath.Join(homeDir, ".parrot", "command_history.json")
43
44 matcher := &EditDistanceMatcher{
45 commandHistory: make([]CommandRecord, 0),
46 maxHistory: 1000, // Keep last 1000 commands
47 similarityThresh: 0.7, // 70% similarity required
48 persistencePath: persistPath,
49 }
50
51 // Try to load existing history
52 matcher.Load()
53
54 return matcher
55 }
56
57 // RecordCommand stores a command failure with its insult
58 func (edm *EditDistanceMatcher) RecordCommand(
59 ctx *SmartFallbackContext,
60 insult string,
61 effectiveness float64,
62 ) {
63 edm.mu.Lock()
64 defer edm.mu.Unlock()
65
66 record := CommandRecord{
67 Command: ctx.FullCommand,
68 CommandType: ctx.CommandType,
69 ErrorPattern: ctx.ErrorPattern,
70 ProjectType: ctx.ProjectType,
71 Timestamp: time.Now(),
72 Insult: insult,
73 Effectiveness: effectiveness,
74 }
75
76 edm.commandHistory = append(edm.commandHistory, record)
77
78 // Prune if too large
79 if len(edm.commandHistory) > edm.maxHistory {
80 // Remove oldest entries
81 edm.commandHistory = edm.commandHistory[len(edm.commandHistory)-edm.maxHistory:]
82 }
83
84 // Periodically persist
85 if len(edm.commandHistory)%50 == 0 {
86 go edm.Save()
87 }
88 }
89
90 // FindSimilarCommands finds commands similar to the current one
91 func (edm *EditDistanceMatcher) FindSimilarCommands(
92 command string,
93 topK int,
94 ) []SimilarCommand {
95 edm.mu.RLock()
96 defer edm.mu.RUnlock()
97
98 if len(edm.commandHistory) == 0 {
99 return nil
100 }
101
102 similarities := make([]SimilarCommand, 0)
103
104 // Calculate similarity to each historical command
105 for _, record := range edm.commandHistory {
106 distance := levenshteinDistanceTier6(command, record.Command)
107 maxLen := maxTier6(len(command), len(record.Command))
108
109 // Calculate similarity (1.0 = identical, 0.0 = completely different)
110 similarity := 1.0 - (float64(distance) / float64(maxLen))
111
112 if similarity >= edm.similarityThresh {
113 similarities = append(similarities, SimilarCommand{
114 Record: record,
115 Similarity: similarity,
116 Distance: distance,
117 })
118 }
119 }
120
121 // Sort by similarity (descending)
122 sortSimilarCommands(similarities)
123
124 // Return top K
125 if len(similarities) > topK {
126 similarities = similarities[:topK]
127 }
128
129 return similarities
130 }
131
132 // GetAdaptedInsult gets an insult adapted from similar command
133 func (edm *EditDistanceMatcher) GetAdaptedInsult(ctx *SmartFallbackContext) string {
134 similar := edm.FindSimilarCommands(ctx.FullCommand, 5)
135
136 if len(similar) == 0 {
137 return ""
138 }
139
140 // Pick the most effective similar command
141 bestIdx := 0
142 bestScore := similar[0].Similarity * similar[0].Record.Effectiveness
143
144 for i, sim := range similar {
145 score := sim.Similarity * sim.Record.Effectiveness
146 if score > bestScore {
147 bestScore = score
148 bestIdx = i
149 }
150 }
151
152 best := similar[bestIdx]
153
154 // Adapt the insult to current context
155 adapted := edm.adaptInsult(best.Record.Insult, best.Record.Command, ctx)
156
157 return adapted
158 }
159
160 // adaptInsult adapts an insult from past command to current context
161 func (edm *EditDistanceMatcher) adaptInsult(
162 insult string,
163 oldCommand string,
164 ctx *SmartFallbackContext,
165 ) string {
166 adapted := insult
167
168 // Extract command differences
169 oldParts := tokenizeCommand(oldCommand)
170 newParts := tokenizeCommand(ctx.FullCommand)
171
172 // Find what changed
173 oldUnique := findUnique(oldParts, newParts)
174 newUnique := findUnique(newParts, oldParts)
175
176 // Replace old unique parts with new ones
177 for i := 0; i < minTier6(len(oldUnique), len(newUnique)); i++ {
178 adapted = replaceWordTier6(adapted, oldUnique[i], newUnique[i])
179 }
180
181 // Add context-specific references if missing
182 if ctx.ProjectType != "" && !containsWordTier6(adapted, ctx.ProjectType) {
183 // Append project context
184 adapted = adapted + " In " + ctx.ProjectType + "."
185 }
186
187 return adapted
188 }
189
190
191 // Save persists command history to disk
192 func (edm *EditDistanceMatcher) Save() error {
193 edm.mu.RLock()
194 defer edm.mu.RUnlock()
195
196 // Ensure directory exists
197 dir := filepath.Dir(edm.persistencePath)
198 os.MkdirAll(dir, 0755)
199
200 // Marshal to JSON
201 data, err := json.MarshalIndent(edm.commandHistory, "", " ")
202 if err != nil {
203 return err
204 }
205
206 // Write atomically
207 tmpPath := edm.persistencePath + ".tmp"
208 if err := os.WriteFile(tmpPath, data, 0644); err != nil {
209 return err
210 }
211
212 return os.Rename(tmpPath, edm.persistencePath)
213 }
214
215 // Load restores command history from disk
216 func (edm *EditDistanceMatcher) Load() error {
217 edm.mu.Lock()
218 defer edm.mu.Unlock()
219
220 data, err := os.ReadFile(edm.persistencePath)
221 if err != nil {
222 return err // File might not exist yet
223 }
224
225 history := make([]CommandRecord, 0)
226 if err := json.Unmarshal(data, &history); err != nil {
227 return err
228 }
229
230 edm.commandHistory = history
231 return nil
232 }
233
234 // GetStats returns matcher statistics
235 func (edm *EditDistanceMatcher) GetStats() map[string]interface{} {
236 edm.mu.RLock()
237 defer edm.mu.RUnlock()
238
239 avgEffectiveness := 0.0
240 if len(edm.commandHistory) > 0 {
241 for _, record := range edm.commandHistory {
242 avgEffectiveness += record.Effectiveness
243 }
244 avgEffectiveness /= float64(len(edm.commandHistory))
245 }
246
247 return map[string]interface{}{
248 "total_commands": len(edm.commandHistory),
249 "avg_effectiveness": avgEffectiveness,
250 "similarity_threshold": edm.similarityThresh,
251 }
252 }
253
254 // Helper functions
255
256 func tokenizeCommand(command string) []string {
257 // Simple tokenization (split on spaces and special chars)
258 tokens := make([]string, 0)
259 current := ""
260
261 for _, r := range command {
262 if r == ' ' || r == '/' || r == '-' || r == '.' {
263 if len(current) > 0 {
264 tokens = append(tokens, current)
265 current = ""
266 }
267 } else {
268 current += string(r)
269 }
270 }
271
272 if len(current) > 0 {
273 tokens = append(tokens, current)
274 }
275
276 return tokens
277 }
278
279 func findUnique(slice1, slice2 []string) []string {
280 unique := make([]string, 0)
281 set2 := make(map[string]bool)
282
283 for _, item := range slice2 {
284 set2[item] = true
285 }
286
287 for _, item := range slice1 {
288 if !set2[item] {
289 unique = append(unique, item)
290 }
291 }
292
293 return unique
294 }
295
296 func sortSimilarCommands(commands []SimilarCommand) {
297 // Bubble sort by similarity (descending)
298 n := len(commands)
299 for i := 0; i < n-1; i++ {
300 for j := 0; j < n-i-1; j++ {
301 if commands[j].Similarity < commands[j+1].Similarity {
302 commands[j], commands[j+1] = commands[j+1], commands[j]
303 }
304 }
305 }
306 }
307