Go · 10591 bytes Raw Blame History
1 package llm
2
3 import (
4 "math"
5 "sort"
6 "strings"
7 )
8
9 // InsultScore represents a scored insult with relevance metrics
10 type InsultScore struct {
11 Insult TaggedInsult
12 TotalScore float64
13 TagMatchScore float64
14 ErrorMatchScore float64
15 ContextScore float64
16 NoveltyScore float64
17 PersonalityScore float64
18 }
19
20 // InsultScorer ranks insults based on multiple factors
21 type InsultScorer struct {
22 database *InsultDatabase
23 errorClassifier *ErrorClassifier
24 intentParser *IntentParser
25 recentInsults []string // Track recent insults to avoid repetition
26 maxRecentHistory int
27 }
28
29 // ScoringWeights defines the weight of each factor in the final score
30 type ScoringWeights struct {
31 TagMatch float64 // How well tags match the context
32 ErrorMatch float64 // How well error type matches
33 Context float64 // Environmental context relevance
34 Novelty float64 // Avoid recent repetition
35 Personality float64 // Match personality preference
36 }
37
38 // DefaultWeights returns the default scoring weights
39 func DefaultWeights() ScoringWeights {
40 return ScoringWeights{
41 TagMatch: 0.35,
42 ErrorMatch: 0.30,
43 Context: 0.20,
44 Novelty: 0.10,
45 Personality: 0.05,
46 }
47 }
48
49 // NewInsultScorer creates a new insult scorer
50 func NewInsultScorer(database *InsultDatabase) *InsultScorer {
51 return &InsultScorer{
52 database: database,
53 errorClassifier: NewErrorClassifier(),
54 intentParser: NewIntentParser(),
55 recentInsults: make([]string, 0, 20),
56 maxRecentHistory: 20,
57 }
58 }
59
60 // ScoreAndRank analyzes context and returns top-ranked insults
61 func (is *InsultScorer) ScoreAndRank(
62 ctx *SmartFallbackContext,
63 personality string,
64 topN int,
65 ) []InsultScore {
66 // Parse command intent
67 intent := is.intentParser.ParseIntent(ctx.FullCommand)
68
69 // Classify error type
70 errorCategories := is.errorClassifier.ClassifyError(
71 ctx.FullCommand,
72 ctx.ExitCode,
73 ctx.ErrorPattern,
74 )
75
76 // Generate contextual tags
77 contextTags := ContextualTags(ctx, intent)
78
79 // Convert error categories to tags
80 errorTags := errorCategoriesToTags(errorCategories)
81
82 // Get all relevant insults
83 allInsults := is.database.Insults
84
85 // Score each insult
86 scores := make([]InsultScore, 0, len(allInsults))
87 weights := DefaultWeights()
88
89 for _, insult := range allInsults {
90 score := InsultScore{
91 Insult: insult,
92 }
93
94 // 1. Tag matching score (35%)
95 score.TagMatchScore = is.calculateTagMatchScore(
96 insult.Tags,
97 contextTags,
98 errorTags,
99 )
100
101 // 2. Error type matching (30%)
102 score.ErrorMatchScore = is.calculateErrorMatchScore(
103 insult.Tags,
104 errorTags,
105 )
106
107 // 3. Context relevance (20%)
108 score.ContextScore = is.calculateContextScore(
109 insult,
110 ctx,
111 intent,
112 )
113
114 // 4. Novelty score (10%) - penalize recent insults
115 score.NoveltyScore = is.calculateNoveltyScore(insult.Text)
116
117 // 5. Personality match (5%)
118 score.PersonalityScore = is.calculatePersonalityScore(
119 insult,
120 personality,
121 )
122
123 // Calculate weighted total
124 score.TotalScore = (score.TagMatchScore * weights.TagMatch) +
125 (score.ErrorMatchScore * weights.ErrorMatch) +
126 (score.ContextScore * weights.Context) +
127 (score.NoveltyScore * weights.Novelty) +
128 (score.PersonalityScore * weights.Personality)
129
130 // Apply base weight from insult
131 score.TotalScore *= insult.Weight
132
133 scores = append(scores, score)
134 }
135
136 // Sort by score descending
137 sort.Slice(scores, func(i, j int) bool {
138 return scores[i].TotalScore > scores[j].TotalScore
139 })
140
141 // Return top N
142 if len(scores) > topN {
143 scores = scores[:topN]
144 }
145
146 return scores
147 }
148
149 // calculateTagMatchScore measures how well insult tags match context
150 func (is *InsultScorer) calculateTagMatchScore(
151 insultTags []InsultTag,
152 contextTags []InsultTag,
153 errorTags []InsultTag,
154 ) float64 {
155 if len(contextTags) == 0 && len(errorTags) == 0 {
156 return 0.5 // Neutral score if no context
157 }
158
159 allSearchTags := append(contextTags, errorTags...)
160 matches := 0
161 totalSearchTags := len(allSearchTags)
162
163 for _, searchTag := range allSearchTags {
164 for _, insultTag := range insultTags {
165 if searchTag == insultTag {
166 matches++
167 break
168 }
169 }
170 }
171
172 // Calculate percentage of search tags that matched
173 score := float64(matches) / float64(totalSearchTags)
174
175 // Bonus for multiple matches
176 if matches > 2 {
177 score = math.Min(1.0, score*1.2)
178 }
179
180 return score
181 }
182
183 // calculateErrorMatchScore focuses specifically on error type matching
184 func (is *InsultScorer) calculateErrorMatchScore(
185 insultTags []InsultTag,
186 errorTags []InsultTag,
187 ) float64 {
188 if len(errorTags) == 0 {
189 return 0.5 // Neutral if no specific error
190 }
191
192 matches := 0
193 for _, errorTag := range errorTags {
194 for _, insultTag := range insultTags {
195 if errorTag == insultTag {
196 matches++
197 }
198 }
199 }
200
201 // Strong match for error-specific insults
202 if matches > 0 {
203 return math.Min(1.0, float64(matches)/float64(len(errorTags))*1.5)
204 }
205
206 return 0.0
207 }
208
209 // calculateContextScore evaluates environmental and situational relevance
210 func (is *InsultScorer) calculateContextScore(
211 insult TaggedInsult,
212 ctx *SmartFallbackContext,
213 intent CommandIntent,
214 ) float64 {
215 score := 0.5 // Base score
216
217 // Time relevance
218 hour := ctx.TimeOfDay
219 if (hour >= 22 || hour <= 4) && hasTag(insult.Tags, TagLateNight) {
220 score += 0.2
221 }
222
223 // CI context
224 if ctx.IsCI && hasTag(insult.Tags, TagCI) {
225 score += 0.2
226 }
227
228 // Main branch failures (more serious)
229 if (ctx.GitBranch == "main" || ctx.GitBranch == "master") &&
230 hasTag(insult.Tags, TagMainBranch) {
231 score += 0.15
232 }
233
234 // Repeated failures
235 if ctx.IsRepeatedFailure && hasTag(insult.Tags, TagRepeated) {
236 score += 0.15
237 }
238
239 // Complexity match
240 if intent.Complexity == "simple" && hasTag(insult.Tags, TagSimple) {
241 score += 0.1
242 } else if intent.Complexity == "complex" && hasTag(insult.Tags, TagComplex) {
243 score += 0.1
244 }
245
246 // High risk operations
247 if intent.RiskLevel == "high" &&
248 (hasTag(insult.Tags, TagProduction) || hasTag(insult.Tags, TagMainBranch)) {
249 score += 0.15
250 }
251
252 return math.Min(1.0, score)
253 }
254
255 // calculateNoveltyScore penalizes recently shown insults
256 func (is *InsultScorer) calculateNoveltyScore(insultText string) float64 {
257 // Check if this insult was shown recently
258 for i, recent := range is.recentInsults {
259 if recent == insultText {
260 // More recent = lower score
261 recency := float64(len(is.recentInsults)-i) / float64(len(is.recentInsults))
262 return 1.0 - (recency * 0.8) // Up to 80% penalty
263 }
264 }
265
266 return 1.0 // Full score for novel insults
267 }
268
269 // calculatePersonalityScore ensures insult matches personality setting
270 func (is *InsultScorer) calculatePersonalityScore(
271 insult TaggedInsult,
272 personality string,
273 ) float64 {
274 switch personality {
275 case "mild":
276 if hasTag(insult.Tags, TagMild) {
277 return 1.0
278 }
279 if insult.Severity <= 4 {
280 return 0.8
281 }
282 return 0.3
283
284 case "sarcastic":
285 if hasTag(insult.Tags, TagSarcastic) {
286 return 1.0
287 }
288 if insult.Severity >= 4 && insult.Severity <= 7 {
289 return 0.8
290 }
291 return 0.5
292
293 case "savage":
294 if hasTag(insult.Tags, TagSavage) {
295 return 1.0
296 }
297 if insult.Severity >= 6 {
298 return 0.8
299 }
300 return 0.4
301
302 default:
303 return 0.7 // Neutral score
304 }
305 }
306
307 // RecordShownInsult adds an insult to recent history
308 func (is *InsultScorer) RecordShownInsult(insultText string) {
309 is.recentInsults = append(is.recentInsults, insultText)
310
311 // Keep only recent N insults
312 if len(is.recentInsults) > is.maxRecentHistory {
313 is.recentInsults = is.recentInsults[1:]
314 }
315 }
316
317 // GetBestInsult returns the top-ranked insult
318 func (is *InsultScorer) GetBestInsult(
319 ctx *SmartFallbackContext,
320 personality string,
321 ) string {
322 scores := is.ScoreAndRank(ctx, personality, 5)
323
324 if len(scores) == 0 {
325 return "Something went wrong. How ironic."
326 }
327
328 bestInsult := scores[0].Insult.Text
329 is.RecordShownInsult(bestInsult)
330
331 return bestInsult
332 }
333
334 // GetTopInsults returns multiple top candidates (useful for variety)
335 func (is *InsultScorer) GetTopInsults(
336 ctx *SmartFallbackContext,
337 personality string,
338 count int,
339 ) []string {
340 scores := is.ScoreAndRank(ctx, personality, count*2)
341
342 results := make([]string, 0, count)
343 for i := 0; i < count && i < len(scores); i++ {
344 results = append(results, scores[i].Insult.Text)
345 }
346
347 return results
348 }
349
350 // Helper functions
351
352 func hasTag(tags []InsultTag, tag InsultTag) bool {
353 for _, t := range tags {
354 if t == tag {
355 return true
356 }
357 }
358 return false
359 }
360
361 func errorCategoriesToTags(categories []ErrorCategory) []InsultTag {
362 tags := make([]InsultTag, 0, len(categories))
363 for _, cat := range categories {
364 tag := errorCategoryToTag(cat)
365 if tag != "" {
366 tags = append(tags, InsultTag(tag))
367 }
368 }
369 return tags
370 }
371
372 func errorCategoryToTag(category ErrorCategory) string {
373 mapping := map[ErrorCategory]string{
374 ErrorPermission: "permission",
375 ErrorSyntax: "syntax",
376 ErrorNetwork: "network",
377 ErrorDependency: "dependency",
378 ErrorMergeConflict: "merge_conflict",
379 ErrorTestFailure: "test_failure",
380 ErrorBuildFailure: "build_failure",
381 ErrorTimeout: "timeout",
382 ErrorAuthentication: "authentication",
383 ErrorDiskSpace: "disk_space",
384 ErrorMemory: "memory",
385 ErrorSegfault: "segfault",
386 ErrorLinting: "linting",
387 ErrorTypeMismatch: "typing",
388 ErrorDeprecated: "deprecated",
389 }
390
391 if tag, exists := mapping[category]; exists {
392 return tag
393 }
394 return ""
395 }
396
397 // AnalyzeScoring provides detailed scoring breakdown (useful for debugging)
398 func (is *InsultScorer) AnalyzeScoring(
399 ctx *SmartFallbackContext,
400 personality string,
401 topN int,
402 ) []InsultScore {
403 return is.ScoreAndRank(ctx, personality, topN)
404 }
405
406 // ClearHistory resets the recent insults history
407 func (is *InsultScorer) ClearHistory() {
408 is.recentInsults = make([]string, 0, is.maxRecentHistory)
409 }
410
411 // LevenshteinDistance calculates similarity between strings (for fuzzy matching)
412 func levenshteinDistance(s1, s2 string) int {
413 s1Lower := strings.ToLower(s1)
414 s2Lower := strings.ToLower(s2)
415
416 m := len(s1Lower)
417 n := len(s2Lower)
418
419 if m == 0 {
420 return n
421 }
422 if n == 0 {
423 return m
424 }
425
426 d := make([][]int, m+1)
427 for i := range d {
428 d[i] = make([]int, n+1)
429 d[i][0] = i
430 }
431 for j := range d[0] {
432 d[0][j] = j
433 }
434
435 for j := 1; j <= n; j++ {
436 for i := 1; i <= m; i++ {
437 cost := 1
438 if s1Lower[i-1] == s2Lower[j-1] {
439 cost = 0
440 }
441 d[i][j] = min(
442 d[i-1][j]+1, // deletion
443 d[i][j-1]+1, // insertion
444 d[i-1][j-1]+cost, // substitution
445 )
446 }
447 }
448
449 return d[m][n]
450 }
451
452 func min(a, b, c int) int {
453 if a < b {
454 if a < c {
455 return a
456 }
457 return c
458 }
459 if b < c {
460 return b
461 }
462 return c
463 }
464