1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum EpisodeOutcome {
10 Success,
12 Partial,
14 Failure,
16}
17
18impl EpisodeOutcome {
19 pub fn score(&self) -> f64 {
21 match self {
22 EpisodeOutcome::Success => 1.0,
23 EpisodeOutcome::Partial => 0.5,
24 EpisodeOutcome::Failure => 0.0,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Episode {
32 pub id: Uuid,
34
35 pub created_at: DateTime<Utc>,
37
38 pub project: Option<String>,
40
41 pub summary: String,
43
44 pub task_type: String,
46
47 pub outcome: EpisodeOutcome,
49
50 pub files_modified: Vec<String>,
52
53 pub errors_resolved: Vec<ErrorResolution>,
55
56 pub tags: Vec<String>,
58
59 pub intent_id: Option<Uuid>,
61
62 pub delta_id: Option<Uuid>,
64
65 pub commit_sha: Option<String>,
67
68 pub utility: f64,
70
71 pub helpful_count: u32,
73
74 pub feedback_count: u32,
76}
77
78impl Episode {
79 pub fn new(summary: String, task_type: String, outcome: EpisodeOutcome) -> Self {
81 Self {
82 id: Uuid::new_v4(),
83 created_at: Utc::now(),
84 project: None,
85 summary,
86 task_type,
87 outcome,
88 files_modified: Vec::new(),
89 errors_resolved: Vec::new(),
90 tags: Vec::new(),
91 intent_id: None,
92 delta_id: None,
93 commit_sha: None,
94 utility: outcome.score(),
95 helpful_count: 0,
96 feedback_count: 0,
97 }
98 }
99
100 pub fn with_project(mut self, project: String) -> Self {
102 self.project = Some(project);
103 self
104 }
105
106 pub fn with_files(mut self, files: Vec<String>) -> Self {
108 self.files_modified = files;
109 self
110 }
111
112 pub fn with_errors(mut self, errors: Vec<ErrorResolution>) -> Self {
114 self.errors_resolved = errors;
115 self
116 }
117
118 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
120 self.tags = tags;
121 self
122 }
123
124 pub fn with_intent(mut self, intent_id: Uuid) -> Self {
126 self.intent_id = Some(intent_id);
127 self
128 }
129
130 pub fn with_delta(mut self, delta_id: Uuid) -> Self {
132 self.delta_id = Some(delta_id);
133 self
134 }
135
136 pub fn with_commit(mut self, sha: String) -> Self {
138 self.commit_sha = Some(sha);
139 self
140 }
141
142 pub fn to_embedding_text(&self) -> String {
144 let mut parts = Vec::new();
145
146 parts.push(self.summary.clone());
147 parts.push(format!("Task: {}", self.task_type));
148
149 if !self.tags.is_empty() {
150 parts.push(format!("Tags: {}", self.tags.join(", ")));
151 }
152
153 if !self.files_modified.is_empty() {
154 let file_names: Vec<&str> = self
156 .files_modified
157 .iter()
158 .filter_map(|f| f.split('/').next_back())
159 .collect();
160 parts.push(format!("Files: {}", file_names.join(", ")));
161 }
162
163 for error in &self.errors_resolved {
164 parts.push(format!("Error: {} -> {}", error.error, error.resolution));
165 }
166
167 parts.join(". ")
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ErrorResolution {
174 pub error: String,
176 pub resolution: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct Feedback {
183 pub episode_id: Uuid,
185 pub timestamp: DateTime<Utc>,
187 pub helpful: bool,
189}
190
191#[derive(Debug, Clone)]
193pub struct RankedEpisode {
194 pub episode: Episode,
196 pub similarity: f64,
198 pub score: f64,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_episode_creation() {
208 let episode = Episode::new(
209 "Fixed authentication bug".to_string(),
210 "bugfix".to_string(),
211 EpisodeOutcome::Success,
212 );
213
214 assert_eq!(episode.summary, "Fixed authentication bug");
215 assert_eq!(episode.task_type, "bugfix");
216 assert_eq!(episode.outcome, EpisodeOutcome::Success);
217 assert_eq!(episode.utility, 1.0);
218 }
219
220 #[test]
221 fn test_episode_builder() {
222 let episode = Episode::new(
223 "Added user profile feature".to_string(),
224 "feature".to_string(),
225 EpisodeOutcome::Success,
226 )
227 .with_project("my-app".to_string())
228 .with_tags(vec!["auth".to_string(), "user".to_string()])
229 .with_files(vec!["src/user.rs".to_string()]);
230
231 assert_eq!(episode.project, Some("my-app".to_string()));
232 assert_eq!(episode.tags.len(), 2);
233 assert_eq!(episode.files_modified.len(), 1);
234 }
235
236 #[test]
237 fn test_embedding_text() {
238 let episode = Episode::new(
239 "Fixed login timeout".to_string(),
240 "bugfix".to_string(),
241 EpisodeOutcome::Success,
242 )
243 .with_tags(vec!["auth".to_string()])
244 .with_files(vec!["src/auth/login.rs".to_string()])
245 .with_errors(vec![ErrorResolution {
246 error: "Connection timeout".to_string(),
247 resolution: "Increased timeout to 30s".to_string(),
248 }]);
249
250 let text = episode.to_embedding_text();
251 assert!(text.contains("Fixed login timeout"));
252 assert!(text.contains("bugfix"));
253 assert!(text.contains("auth"));
254 assert!(text.contains("login.rs"));
255 assert!(text.contains("Connection timeout"));
256 }
257
258 #[test]
259 fn test_outcome_score() {
260 assert_eq!(EpisodeOutcome::Success.score(), 1.0);
261 assert_eq!(EpisodeOutcome::Partial.score(), 0.5);
262 assert_eq!(EpisodeOutcome::Failure.score(), 0.0);
263 }
264}