1use crate::core::provider_bandit::ProviderBandit;
17
18#[derive(Debug, Clone)]
20pub struct PreloadPrediction {
21 pub provider_id: String,
22 pub action: String,
23 pub confidence: f64,
24 pub reason: String,
25}
26
27static KEYWORD_MAPPINGS: &[(&[&str], &str, &str)] = &[
29 (
30 &["bug", "error", "crash", "fix", "broken", "issue", "defect"],
31 "github",
32 "issues",
33 ),
34 (
35 &["bug", "error", "crash", "fix", "broken", "issue", "defect"],
36 "jira",
37 "issues",
38 ),
39 (
40 &["pr", "pull", "merge", "review", "branch"],
41 "github",
42 "pull_requests",
43 ),
44 (
45 &[
46 "database",
47 "table",
48 "schema",
49 "column",
50 "migration",
51 "sql",
52 "db",
53 ],
54 "postgres",
55 "schemas",
56 ),
57 (
58 &["sprint", "story", "epic", "velocity", "backlog"],
59 "jira",
60 "sprints",
61 ),
62 (
63 &["wiki", "doc", "documentation", "guide", "howto"],
64 "github",
65 "issues",
66 ),
67];
68
69pub fn predict_preloads(
71 task_description: &str,
72 available_providers: &[String],
73 bandit: &mut ProviderBandit,
74 max_predictions: usize,
75) -> Vec<PreloadPrediction> {
76 let task_lower = task_description.to_lowercase();
77 let task_words: Vec<&str> = task_lower.split_whitespace().collect();
78
79 let mut predictions: Vec<PreloadPrediction> = Vec::new();
80
81 for &(keywords, provider, action) in KEYWORD_MAPPINGS {
82 if !available_providers.iter().any(|p| p == provider) {
83 continue;
84 }
85
86 let matching_keywords: Vec<&&str> = keywords
87 .iter()
88 .filter(|kw| task_words.iter().any(|tw| tw.contains(*kw)))
89 .collect();
90
91 if matching_keywords.is_empty() {
92 continue;
93 }
94
95 let keyword_confidence = matching_keywords.len() as f64 / keywords.len() as f64;
96
97 let task_type = infer_task_type(&task_lower);
98 let bandit_score = bandit.estimated_probability(&task_type, provider);
99
100 let combined = 0.6 * keyword_confidence + 0.4 * bandit_score;
101
102 if !predictions
103 .iter()
104 .any(|p| p.provider_id == provider && p.action == action)
105 {
106 predictions.push(PreloadPrediction {
107 provider_id: provider.to_string(),
108 action: action.to_string(),
109 confidence: combined,
110 reason: format!(
111 "keywords: {}",
112 matching_keywords
113 .iter()
114 .map(|k| **k)
115 .collect::<Vec<_>>()
116 .join(", ")
117 ),
118 });
119 }
120 }
121
122 predictions.sort_by(|a, b| {
123 b.confidence
124 .partial_cmp(&a.confidence)
125 .unwrap_or(std::cmp::Ordering::Equal)
126 });
127 predictions.truncate(max_predictions);
128 predictions
129}
130
131#[must_use]
134pub fn infer_task_type(task: &str) -> String {
135 if task.contains("bug")
136 || task.contains("fix")
137 || task.contains("error")
138 || task.contains("crash")
139 {
140 "bugfix".into()
141 } else if task.contains("feature") || task.contains("add") || task.contains("implement") {
142 "feature".into()
143 } else if task.contains("refactor") || task.contains("clean") || task.contains("improve") {
144 "refactor".into()
145 } else {
146 "general".into()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn predict_bug_fix_suggests_issues() {
156 let mut bandit = ProviderBandit::new();
157 let providers = vec!["github".into(), "jira".into()];
158
159 let predictions = predict_preloads(
160 "Fix the authentication bug in the login flow",
161 &providers,
162 &mut bandit,
163 5,
164 );
165
166 assert!(!predictions.is_empty());
167 assert!(
168 predictions
169 .iter()
170 .any(|p| p.provider_id == "github" && p.action == "issues")
171 );
172 }
173
174 #[test]
175 fn predict_db_task_suggests_schemas() {
176 let mut bandit = ProviderBandit::new();
177 let providers = vec!["postgres".into()];
178
179 let predictions = predict_preloads(
180 "Add a new column to the users database table",
181 &providers,
182 &mut bandit,
183 5,
184 );
185
186 assert!(
187 predictions
188 .iter()
189 .any(|p| p.provider_id == "postgres" && p.action == "schemas")
190 );
191 }
192
193 #[test]
194 fn predict_pr_review_suggests_pull_requests() {
195 let mut bandit = ProviderBandit::new();
196 let providers = vec!["github".into()];
197
198 let predictions = predict_preloads(
199 "Review the open pull requests and merge the approved ones",
200 &providers,
201 &mut bandit,
202 5,
203 );
204
205 assert!(
206 predictions
207 .iter()
208 .any(|p| p.provider_id == "github" && p.action == "pull_requests")
209 );
210 }
211
212 #[test]
213 fn predict_empty_task_returns_empty() {
214 let mut bandit = ProviderBandit::new();
215 let predictions = predict_preloads("", &["github".into()], &mut bandit, 5);
216 assert!(predictions.is_empty());
217 }
218
219 #[test]
220 fn predict_unavailable_provider_skipped() {
221 let mut bandit = ProviderBandit::new();
222 let predictions = predict_preloads(
223 "Fix the database schema migration",
224 &["github".into()], &mut bandit,
226 5,
227 );
228
229 assert!(!predictions.iter().any(|p| p.provider_id == "postgres"));
230 }
231
232 #[test]
233 fn predict_respects_max_predictions() {
234 let mut bandit = ProviderBandit::new();
235 let providers = vec!["github".into(), "jira".into(), "postgres".into()];
236
237 let predictions = predict_preloads(
238 "Fix the bug in database schema and review pull requests",
239 &providers,
240 &mut bandit,
241 2,
242 );
243
244 assert!(predictions.len() <= 2);
245 }
246
247 #[test]
248 fn predict_bandit_trained_boosts_confidence() {
249 let mut bandit = ProviderBandit::new();
250 for _ in 0..20 {
251 bandit.update("bugfix", "github", true);
252 bandit.update("bugfix", "jira", false);
253 }
254
255 let providers = vec!["github".into(), "jira".into()];
256 let predictions = predict_preloads(
257 "Fix the crash bug in authentication",
258 &providers,
259 &mut bandit,
260 5,
261 );
262
263 let gh = predictions
264 .iter()
265 .find(|p| p.provider_id == "github" && p.action == "issues");
266 let jira = predictions
267 .iter()
268 .find(|p| p.provider_id == "jira" && p.action == "issues");
269
270 if let (Some(gh), Some(jira)) = (gh, jira) {
271 assert!(
272 gh.confidence > jira.confidence,
273 "Trained bandit should boost github over jira"
274 );
275 }
276 }
277
278 #[test]
279 fn infer_task_type_correctness() {
280 assert_eq!(infer_task_type("fix the crash bug"), "bugfix");
281 assert_eq!(infer_task_type("add new feature"), "feature");
282 assert_eq!(infer_task_type("refactor the auth module"), "refactor");
283 assert_eq!(infer_task_type("update documentation"), "general");
284 }
285}