1use super::decay::{apply_decay, DecayParams};
4use super::wilson::wilson_score_default;
5use crate::types::{Episode, RankedEpisode};
6use chrono::Utc;
7
8#[derive(Debug, Clone)]
10pub struct RankerConfig {
11 pub similarity_weight: f64,
13 pub utility_weight: f64,
15 pub feedback_weight: f64,
17 pub decay_params: DecayParams,
19}
20
21impl Default for RankerConfig {
22 fn default() -> Self {
23 Self {
24 similarity_weight: 0.5,
25 utility_weight: 0.3,
26 feedback_weight: 0.2,
27 decay_params: DecayParams::default(),
28 }
29 }
30}
31
32pub struct UtilityRanker {
34 config: RankerConfig,
35}
36
37impl UtilityRanker {
38 pub fn new() -> Self {
40 Self {
41 config: RankerConfig::default(),
42 }
43 }
44
45 pub fn with_config(config: RankerConfig) -> Self {
47 Self { config }
48 }
49
50 pub fn rank(&self, episodes: Vec<Episode>, similarities: Vec<f64>) -> Vec<RankedEpisode> {
59 let now = Utc::now();
60
61 let mut ranked: Vec<RankedEpisode> = episodes
62 .into_iter()
63 .zip(similarities)
64 .map(|(episode, similarity)| {
65 let score = self.compute_score(&episode, similarity, now);
66 RankedEpisode {
67 episode,
68 similarity,
69 score,
70 }
71 })
72 .collect();
73
74 ranked.sort_by(|a, b| {
76 b.score
77 .partial_cmp(&a.score)
78 .unwrap_or(std::cmp::Ordering::Equal)
79 });
80
81 ranked
82 }
83
84 fn compute_score(&self, episode: &Episode, similarity: f64, now: chrono::DateTime<Utc>) -> f64 {
86 let decayed_utility = apply_decay(
88 episode.utility,
89 episode.created_at,
90 now,
91 &self.config.decay_params,
92 );
93
94 let feedback_score = wilson_score_default(episode.helpful_count, episode.feedback_count);
96
97 self.config.similarity_weight * similarity
99 + self.config.utility_weight * decayed_utility
100 + self.config.feedback_weight * feedback_score
101 }
102
103 pub fn update_utility_from_feedback(
107 &self,
108 episode: &Episode,
109 helpful: bool,
110 learning_rate: f64,
111 ) -> f64 {
112 let feedback_value = if helpful { 1.0 } else { 0.0 };
113 let new_utility = episode.utility + learning_rate * (feedback_value - episode.utility);
114 new_utility.clamp(0.0, 1.0)
115 }
116}
117
118impl Default for UtilityRanker {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::types::EpisodeOutcome;
128
129 fn make_episode(summary: &str, utility: f64, helpful: u32, total: u32) -> Episode {
130 let mut ep = Episode::new(
131 summary.to_string(),
132 "test".to_string(),
133 EpisodeOutcome::Success,
134 );
135 ep.utility = utility;
136 ep.helpful_count = helpful;
137 ep.feedback_count = total;
138 ep
139 }
140
141 #[test]
142 fn test_ranking_by_similarity() {
143 let ranker = UtilityRanker::new();
144
145 let ep1 = make_episode("Episode 1", 0.5, 0, 0);
146 let ep2 = make_episode("Episode 2", 0.5, 0, 0);
147
148 let episodes = vec![ep1, ep2];
149 let similarities = vec![0.9, 0.5]; let ranked = ranker.rank(episodes, similarities);
152
153 assert_eq!(ranked[0].episode.summary, "Episode 1");
154 assert!(ranked[0].score > ranked[1].score);
155 }
156
157 #[test]
158 fn test_ranking_considers_utility() {
159 let ranker = UtilityRanker::new();
160
161 let ep1 = make_episode("Low utility", 0.1, 0, 0);
162 let ep2 = make_episode("High utility", 0.9, 0, 0);
163
164 let episodes = vec![ep1, ep2];
165 let similarities = vec![0.8, 0.8]; let ranked = ranker.rank(episodes, similarities);
168
169 assert_eq!(ranked[0].episode.summary, "High utility");
171 }
172
173 #[test]
174 fn test_ranking_considers_feedback() {
175 let ranker = UtilityRanker::new();
176
177 let ep1 = make_episode("Poor feedback", 0.5, 1, 10); let ep2 = make_episode("Good feedback", 0.5, 9, 10); let episodes = vec![ep1, ep2];
181 let similarities = vec![0.8, 0.8]; let ranked = ranker.rank(episodes, similarities);
184
185 assert_eq!(ranked[0].episode.summary, "Good feedback");
187 }
188
189 #[test]
190 fn test_utility_update() {
191 let ranker = UtilityRanker::new();
192
193 let episode = make_episode("Test", 0.5, 0, 0);
194
195 let new_utility = ranker.update_utility_from_feedback(&episode, true, 0.1);
197 assert!(new_utility > 0.5);
198
199 let new_utility = ranker.update_utility_from_feedback(&episode, false, 0.1);
201 assert!(new_utility < 0.5);
202 }
203
204 #[test]
205 fn test_utility_bounds() {
206 let ranker = UtilityRanker::new();
207
208 let high = make_episode("High", 0.99, 0, 0);
209 let low = make_episode("Low", 0.01, 0, 0);
210
211 let new_high = ranker.update_utility_from_feedback(&high, true, 0.5);
213 assert!(new_high <= 1.0);
214
215 let new_low = ranker.update_utility_from_feedback(&low, false, 0.5);
217 assert!(new_low >= 0.0);
218 }
219}