Go · 6543 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 // InsultHistory tracks shown insults to avoid repetition
12 type InsultHistory struct {
13 RecentInsults []HistoryEntry `json:"recent_insults"`
14 InsultFrequency map[string]int `json:"insult_frequency"` // How many times each shown
15 LastCleanup time.Time `json:"last_cleanup"`
16 mu sync.RWMutex
17 maxHistory int
18 persistPath string
19 }
20
21 // HistoryEntry represents a single shown insult
22 type HistoryEntry struct {
23 Text string `json:"text"`
24 Timestamp time.Time `json:"timestamp"`
25 Context string `json:"context"` // Command that triggered it
26 Score float64 `json:"score"` // Relevance score
27 }
28
29 // NewInsultHistory creates a new history tracker
30 func NewInsultHistory(maxHistory int) *InsultHistory {
31 homeDir, err := os.UserHomeDir()
32 if err != nil {
33 homeDir = "."
34 }
35
36 persistPath := filepath.Join(homeDir, ".parrot", "insult_history.json")
37
38 history := &InsultHistory{
39 RecentInsults: make([]HistoryEntry, 0, maxHistory),
40 InsultFrequency: make(map[string]int),
41 maxHistory: maxHistory,
42 persistPath: persistPath,
43 LastCleanup: time.Now(),
44 }
45
46 // Try to load existing history
47 history.Load()
48
49 return history
50 }
51
52 // RecordInsult adds an insult to the history
53 func (h *InsultHistory) RecordInsult(text string, context string, score float64) {
54 h.mu.Lock()
55 defer h.mu.Unlock()
56
57 entry := HistoryEntry{
58 Text: text,
59 Timestamp: time.Now(),
60 Context: context,
61 Score: score,
62 }
63
64 h.RecentInsults = append(h.RecentInsults, entry)
65
66 // Trim if exceeds max
67 if len(h.RecentInsults) > h.maxHistory {
68 h.RecentInsults = h.RecentInsults[1:]
69 }
70
71 // Update frequency
72 h.InsultFrequency[text]++
73
74 // Periodic cleanup (once per day)
75 if time.Since(h.LastCleanup) > 24*time.Hour {
76 h.cleanup()
77 }
78
79 // Persist to disk
80 h.persist()
81 }
82
83 // WasRecentlyShown checks if an insult was shown recently
84 func (h *InsultHistory) WasRecentlyShown(text string, withinLast int) bool {
85 h.mu.RLock()
86 defer h.mu.RUnlock()
87
88 if withinLast > len(h.RecentInsults) {
89 withinLast = len(h.RecentInsults)
90 }
91
92 startIdx := len(h.RecentInsults) - withinLast
93 if startIdx < 0 {
94 startIdx = 0
95 }
96
97 for i := startIdx; i < len(h.RecentInsults); i++ {
98 if h.RecentInsults[i].Text == text {
99 return true
100 }
101 }
102
103 return false
104 }
105
106 // GetRecencyScore returns a score (0-1) based on how recently insult was shown
107 // 0 = shown very recently, 1 = never shown or shown long ago
108 func (h *InsultHistory) GetRecencyScore(text string) float64 {
109 h.mu.RLock()
110 defer h.mu.RUnlock()
111
112 // Find the most recent occurrence
113 for i := len(h.RecentInsults) - 1; i >= 0; i-- {
114 if h.RecentInsults[i].Text == text {
115 // Calculate position from end (more recent = lower score)
116 position := len(h.RecentInsults) - i
117 // Normalize to 0-1 range
118 recencyPenalty := float64(position) / float64(h.maxHistory)
119 // Invert so recent = low score
120 return 1.0 - recencyPenalty
121 }
122 }
123
124 return 1.0 // Not found = full score
125 }
126
127 // GetFrequencyScore returns a score based on how often insult was used
128 // Less frequent = higher score
129 func (h *InsultHistory) GetFrequencyScore(text string) float64 {
130 h.mu.RLock()
131 defer h.mu.RUnlock()
132
133 frequency := h.InsultFrequency[text]
134 if frequency == 0 {
135 return 1.0 // Never shown
136 }
137
138 // Find max frequency for normalization
139 maxFreq := 1
140 for _, freq := range h.InsultFrequency {
141 if freq > maxFreq {
142 maxFreq = freq
143 }
144 }
145
146 // Normalize and invert (higher frequency = lower score)
147 return 1.0 - (float64(frequency) / float64(maxFreq) * 0.7) // Max 70% penalty
148 }
149
150 // GetNoveltyScore combines recency and frequency for overall novelty
151 func (h *InsultHistory) GetNoveltyScore(text string) float64 {
152 recency := h.GetRecencyScore(text)
153 frequency := h.GetFrequencyScore(text)
154
155 // Weight recency more heavily (70% recency, 30% frequency)
156 return (recency * 0.7) + (frequency * 0.3)
157 }
158
159 // cleanup removes old entries and resets counters
160 func (h *InsultHistory) cleanup() {
161 // Remove entries older than 7 days
162 cutoff := time.Now().AddDate(0, 0, -7)
163 validEntries := make([]HistoryEntry, 0)
164
165 for _, entry := range h.RecentInsults {
166 if entry.Timestamp.After(cutoff) {
167 validEntries = append(validEntries, entry)
168 }
169 }
170
171 h.RecentInsults = validEntries
172
173 // Reset frequency counters for removed insults
174 newFrequency := make(map[string]int)
175 for _, entry := range h.RecentInsults {
176 newFrequency[entry.Text]++
177 }
178 h.InsultFrequency = newFrequency
179
180 h.LastCleanup = time.Now()
181 }
182
183 // persist saves history to disk
184 func (h *InsultHistory) persist() {
185 // Create directory if it doesn't exist
186 dir := filepath.Dir(h.persistPath)
187 if err := os.MkdirAll(dir, 0755); err != nil {
188 return // Silently fail if can't create directory
189 }
190
191 data, err := json.MarshalIndent(h, "", " ")
192 if err != nil {
193 return
194 }
195
196 // Write to temp file first, then rename (atomic)
197 tempPath := h.persistPath + ".tmp"
198 if err := os.WriteFile(tempPath, data, 0644); err != nil {
199 return
200 }
201
202 os.Rename(tempPath, h.persistPath)
203 }
204
205 // Load reads history from disk
206 func (h *InsultHistory) Load() error {
207 h.mu.Lock()
208 defer h.mu.Unlock()
209
210 data, err := os.ReadFile(h.persistPath)
211 if err != nil {
212 return err // File doesn't exist or can't be read
213 }
214
215 // Create a temporary struct to unmarshal into
216 var loaded struct {
217 RecentInsults []HistoryEntry `json:"recent_insults"`
218 InsultFrequency map[string]int `json:"insult_frequency"`
219 LastCleanup time.Time `json:"last_cleanup"`
220 }
221
222 if err := json.Unmarshal(data, &loaded); err != nil {
223 return err
224 }
225
226 h.RecentInsults = loaded.RecentInsults
227 h.InsultFrequency = loaded.InsultFrequency
228 h.LastCleanup = loaded.LastCleanup
229
230 return nil
231 }
232
233 // Clear removes all history
234 func (h *InsultHistory) Clear() {
235 h.mu.Lock()
236 defer h.mu.Unlock()
237
238 h.RecentInsults = make([]HistoryEntry, 0, h.maxHistory)
239 h.InsultFrequency = make(map[string]int)
240 h.LastCleanup = time.Now()
241
242 h.persist()
243 }
244
245 // GetStats returns statistics about insult history
246 func (h *InsultHistory) GetStats() map[string]interface{} {
247 h.mu.RLock()
248 defer h.mu.RUnlock()
249
250 // Find most frequent insult
251 mostFrequent := ""
252 maxFreq := 0
253 for text, freq := range h.InsultFrequency {
254 if freq > maxFreq {
255 maxFreq = freq
256 mostFrequent = text
257 }
258 }
259
260 return map[string]interface{}{
261 "total_insults_shown": len(h.RecentInsults),
262 "unique_insults": len(h.InsultFrequency),
263 "most_frequent": mostFrequent,
264 "most_frequent_count": maxFreq,
265 "last_cleanup": h.LastCleanup,
266 }
267 }
268