1use crate::utils::current_timestamp;
7use serde::{Deserialize, Serialize};
8use smallvec::SmallVec;
9
10pub fn jaro_winkler_similarity(s1: &str, s2: &str) -> f32 {
18 strsim::jaro_winkler(s1, s2) as f32
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct TimeDecayedScore {
29 pub base_score: f32,
31 pub age_seconds: u64,
33 pub decay_lambda: f32,
35 pub decayed_score: f32,
37}
38
39impl TimeDecayedScore {
40 pub fn calculate(base_score: f32, timestamp: u64) -> Self {
42 const DEFAULT_LAMBDA: f32 = 0.1;
43 let now = current_timestamp();
44 let age_seconds = now.saturating_sub(timestamp);
45 let age_hours = age_seconds as f32 / 3600.0;
46 let decayed_score = (base_score * (-DEFAULT_LAMBDA * age_hours).exp()).clamp(0.0, 1.0);
47
48 Self {
49 base_score,
50 age_seconds,
51 decay_lambda: DEFAULT_LAMBDA,
52 decayed_score,
53 }
54 }
55
56 pub fn with_decay(mut self, lambda: f32) -> Self {
58 let age_hours = self.age_seconds as f32 / 3600.0;
59 self.decayed_score = (self.base_score * (-lambda * age_hours).exp()).clamp(0.0, 1.0);
60 self.decay_lambda = lambda;
61 self
62 }
63}
64
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
69pub enum PatternState {
70 Single,
72 Duplicate,
74 Loop,
76 NearLoop,
78 RefinementChain,
80 Convergence,
82 Degradation,
84}
85
86#[derive(Debug, Clone)]
91pub struct ToolCallRecord {
92 pub tool: String,
94 pub args_hash: String,
96 pub quality: f32,
98}
99
100pub fn detect_pattern(history: &[ToolCallRecord], window_size: usize) -> PatternState {
109 if history.is_empty() {
110 return PatternState::Single;
111 }
112
113 let start = history.len().saturating_sub(window_size);
116 let recent = &history[start..];
117
118 if recent.len() < 2 {
119 return PatternState::Single;
120 }
121
122 let first = &recent[0];
123
124 if recent.iter().all(|r| r.tool == first.tool && r.args_hash == first.args_hash) {
126 return if recent.len() >= 3 {
127 PatternState::Loop
128 } else {
129 PatternState::Duplicate
130 };
131 }
132
133 let mut qualities = SmallVec::<[f32; 32]>::with_capacity(recent.len());
136 let mut same_tool = true;
137 let mut multi_tool = false;
138
139 for r in recent {
140 qualities.push(r.quality);
141 if r.tool != first.tool {
142 same_tool = false;
143 multi_tool = true;
144 }
145 }
146
147 if qualities.len() >= 3 {
149 if qualities.windows(2).all(|w| w[1] > w[0] + 0.05) {
150 return PatternState::RefinementChain;
151 }
152 if qualities.windows(2).all(|w| w[1] < w[0] - 0.05) {
153 return PatternState::Degradation;
154 }
155 }
156
157 if same_tool
160 && recent.len() >= 3
161 && recent
162 .windows(2)
163 .all(|w| jaro_winkler_similarity(&w[0].args_hash, &w[1].args_hash) > 0.85)
164 {
165 return PatternState::NearLoop;
166 }
167
168 if multi_tool {
170 let n = qualities.len() as f32;
171 let avg = qualities.iter().sum::<f32>() / n;
172 if qualities.iter().all(|&q| (q - avg).abs() < 0.1) {
173 return PatternState::Convergence;
174 }
175 }
176
177 PatternState::Single
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct MLScoreComponents {
187 pub success_rate: f32,
189 pub avg_execution_time: f32,
191 pub result_quality: f32,
193 pub failure_count: usize,
195 pub age_hours: f32,
197 pub frequency: f32,
199 pub confidence: f32,
201}
202
203impl MLScoreComponents {
204 pub fn raw_score(&self) -> f32 {
208 (self.success_rate * 0.40)
209 + (self.result_quality * 0.30)
210 + ((10_000.0 - self.avg_execution_time).max(0.0) / 10_000.0 * 0.15)
211 + (self.frequency.min(1.0) * 0.15)
212 }
213
214 pub fn with_age_decay(mut self) -> Self {
216 self.confidence = (self.confidence * (-self.age_hours / 168.0).exp()).max(0.1);
218 self
219 }
220
221 pub fn to_feature_vector(&self) -> [f32; 7] {
235 [
236 self.success_rate,
237 self.avg_execution_time / 10_000.0,
238 self.result_quality,
239 (self.failure_count as f32).min(10.0) / 10.0,
240 self.age_hours / 168.0,
241 self.frequency,
242 self.confidence,
243 ]
244 }
245}
246
247#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_jaro_winkler_exact() {
255 assert!((jaro_winkler_similarity("hello", "hello") - 1.0).abs() < f32::EPSILON);
256 }
257
258 #[test]
259 fn test_jaro_winkler_partial() {
260 let sim = jaro_winkler_similarity("pattern", "pattern_file");
261 assert!(sim > 0.85 && sim < 1.0, "sim={sim}");
262 }
263
264 #[test]
265 fn test_jaro_winkler_prefix_boost() {
266 let with_prefix = jaro_winkler_similarity("test_one", "test_two");
267 let without = jaro_winkler_similarity("one_test", "two_test");
268 assert!(with_prefix > without, "prefix boost should be applied");
269 }
270
271 #[test]
272 fn test_time_decay_ordering() {
273 let now = current_timestamp();
274 let recent = TimeDecayedScore::calculate(0.9, now);
275 let old = TimeDecayedScore::calculate(0.9, now.saturating_sub(7 * 24 * 3600));
276 assert!(old.decayed_score < recent.decayed_score);
277 }
278
279 #[test]
280 fn test_detect_pattern_loop() {
281 let history = vec![
282 ToolCallRecord {
283 tool: "grep".into(),
284 args_hash: "pattern1".into(),
285 quality: 0.5,
286 },
287 ToolCallRecord {
288 tool: "grep".into(),
289 args_hash: "pattern1".into(),
290 quality: 0.5,
291 },
292 ToolCallRecord {
293 tool: "grep".into(),
294 args_hash: "pattern1".into(),
295 quality: 0.5,
296 },
297 ];
298 assert_eq!(detect_pattern(&history, 10), PatternState::Loop);
299 }
300
301 #[test]
302 fn test_detect_pattern_refinement() {
303 let history = vec![
304 ToolCallRecord {
305 tool: "grep".into(),
306 args_hash: "pat1".into(),
307 quality: 0.3,
308 },
309 ToolCallRecord {
310 tool: "grep".into(),
311 args_hash: "pat2".into(),
312 quality: 0.5,
313 },
314 ToolCallRecord {
315 tool: "grep".into(),
316 args_hash: "pat3".into(),
317 quality: 0.8,
318 },
319 ];
320 assert_eq!(detect_pattern(&history, 10), PatternState::RefinementChain);
321 }
322
323 #[test]
324 fn test_detect_pattern_near_loop_requires_three_entries() {
325 let two_entries = vec![
326 ToolCallRecord {
327 tool: "grep".into(),
328 args_hash: "pattern-one".into(),
329 quality: 0.4,
330 },
331 ToolCallRecord {
332 tool: "grep".into(),
333 args_hash: "pattern-two".into(),
334 quality: 0.45,
335 },
336 ];
337 assert_eq!(detect_pattern(&two_entries, 10), PatternState::Single);
338
339 let three_entries = vec![
340 ToolCallRecord {
341 tool: "grep".into(),
342 args_hash: "pattern-one".into(),
343 quality: 0.4,
344 },
345 ToolCallRecord {
346 tool: "grep".into(),
347 args_hash: "pattern-two".into(),
348 quality: 0.45,
349 },
350 ToolCallRecord {
351 tool: "grep".into(),
352 args_hash: "pattern-three".into(),
353 quality: 0.5,
354 },
355 ];
356 assert_eq!(detect_pattern(&three_entries, 10), PatternState::NearLoop);
357 }
358
359 #[test]
360 fn test_ml_raw_score() {
361 let c = MLScoreComponents {
362 success_rate: 0.9,
363 avg_execution_time: 100.0,
364 result_quality: 0.85,
365 failure_count: 1,
366 age_hours: 2.0,
367 frequency: 0.5,
368 confidence: 0.9,
369 };
370 let score = c.raw_score();
371 assert!(score > 0.7 && score < 1.0, "score={score}");
372 }
373
374 #[test]
375 fn test_ml_feature_vector_length() {
376 let c = MLScoreComponents {
377 success_rate: 0.9,
378 avg_execution_time: 100.0,
379 result_quality: 0.85,
380 failure_count: 1,
381 age_hours: 2.0,
382 frequency: 0.5,
383 confidence: 0.9,
384 };
385 assert_eq!(c.to_feature_vector().len(), 7);
386 }
387}